The Nostr protocol, natively in Zig.
nostr is a foundational Nostr protocol library for
Zig: keys and signatures, events, relay transport with
the outbox model, a zero-copy local-first event store, and the NIP-46
remote-signing protocol. It's the base layer a fast, native Nostr app is built
on: no browser, no Electron, the protocol running close to the metal.
It's also the core of a small ecosystem: the library, plus native apps built on it like Notary, a remote signer that keeps your key off every client. Full docs, benchmarks, and the ecosystem overview live at zignostr.com.
Status: early (
v0.16.0). The library core, transport, local-first store and signer protocol have shipped and are covered by tests. Two native apps run on it today. APIs may still change before 1.0.
- π Credible crypto core: secp256k1 keys and BIP-340 Schnorr signatures
via bitcoin-core's
libsecp256k1, passing the full official test-vector suite (19/19). - β‘ Local-first store: a zero-copy, memory-mapped LMDB event store with a bounded, newest-first query planner: sub-millisecond feeds that stay flat as the store grows.
- π§ Outbox model: NIP-65 relay lists with read/write routing and zero hardcoded relays; events go where they belong.
- π Live transport: RFC 6455 WebSocket over TCP/TLS with NIP-01 subscriptions, and a keepalive that separates a connection which has died from one that is merely quiet. A socket that stops answering is noticed rather than believed.
- π Encrypted payloads: NIP-44 v2 authenticated encryption (ChaCha20 + HMAC-SHA256), verified against the official vectors.
- π‘οΈ Remote signing: the NIP-46 "bunker" protocol plus NIP-42 relay auth, so a key can sign for any client without ever entering it.
- π Portable keys: NIP-06 mnemonic derivation and NIP-49 (
ncryptsec) encrypted key storage, NFKC-normalized for cross-app interop. - βοΈ Private messages: NIP-59 gift wrap and NIP-17 direct messages, with NIP-04 for the older clients that still send it. An unwrap refuses a message whose author is not the one who sealed it.
- π Set sync: NIP-77 negentropy, matching the hoytech reference message for message, with a storage that reads its items straight from the local store.
- π§ Writing a relay:
message.parseClientMessageand the relay-side encoders (OK,EOSE,CLOSED,NOTICE,AUTH, NIP-45COUNT, NIP-77NEG-MSG), plus the NIP-86 management API and NIP-98 verification, so a relay can be built on the same codecs a client uses. - π Remote signer client: the app side of NIP-46, for
bunker://andnostrconnect://, with one reader task per relay. - βοΈ Proof of work: NIP-13 with a multi-threaded miner that hashes from a SHA-256 midstate.
- π HTTP codecs: NIP-05 identifiers, NIP-11 relay information, NIP-98 HTTP auth and Blossom auth tokens. No HTTP happens inside the library: you fetch, these parse and build.
ARCHITECTURE.md lists the modules, follows an event from a key to a relay to the store, and explains the threading and cancellation rules and how the library is tested, fuzz targets included.
macOS, Linux and Windows, on x86_64 and aarch64. CI builds and runs the tests on all three systems. Zig 0.16 is required, at the version pinned in .zigversion. To check a Windows build from another system, zig build test -Dtarget=x86_64-windows-gnu compiles the test binary and then stops with an error, because a non-Windows host cannot run it.
Performance is a design goal, not an afterthought. These are the library's own
numbers, measured with the in-repo benchmark and reproducible on your machine
(Apple Silicon, ReleaseFast, warm cache, best of 50; events spread across 100
authors; a 20-author kind:1 feed returning 500 notes):
| Store size | Feed query (500 notes) | Timeline query (1 author) | Profile query | Ingest |
|---|---|---|---|---|
| 20,000 events | 0.25 ms | 0.09 ms | 0.007 ms | ~170k events/s |
| 100,000 events | 0.28 ms | 0.24 ms | 0.008 ms | ~149k events/s |
The headline isn't the ~0.28 ms feed query, it's that 5x more stored events
barely moves it. The bounded query planner walks the indexes newest-first and
stops at limit, so latency tracks the page size you ask for, not the size of
the store.
The profile query is the same point made sharply. Fetching one account's
kind:0 out of a hundred thousand events reads exactly one index entry, no
matter how much that account has posted since they set it, because a filter
naming both authors and kinds is served by an index on the pair. The benchmark
prints that entry count next to the latency: it is the part a stopwatch cannot
tell you.
BENCH_N=100000 zig build bench -Doptimize=ReleaseFastMethodology and the full write-up are on the benchmarks page.
An AI coding agent working on a Zig nostr project can learn this library from the skill in skills/zig-nostr: adding the dependency, keys and events, NIP-19 and NIP-44, relays read with a deadline, the local store, and the traps, with every snippet compiled against the current release. Add it to any agent that supports skills:
npx skills add zig-nostr/nostrFor work from a shell rather than in Zig, deed is a command line built on this library, with a skill of its own.
Add the library to your build.zig.zon:
zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.16.0.tar.gzWire the module in build.zig:
const nostr = b.dependency("nostr", .{ .target = target, .optimize = optimize });
your_module.addImport("nostr", nostr.module("nostr"));Sign and verify an event:
const std = @import("std");
const nostr = @import("nostr");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Time comes from the Io implementation in Zig 0.16, the same way sockets
// and threads do. Threaded takes a threadsafe allocator, which the arena
// above is not.
var threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
defer threaded.deinit();
const io = threaded.io();
// A Signer wraps the libsecp256k1 context; deinit it when done.
var signer = nostr.keys.Signer.init();
defer signer.deinit();
// Load a keypair from a 32-byte secret key.
const secret: nostr.keys.SecretKey = your_secret_key; // 32 bytes
const keypair = try signer.keyPairFromSecretKey(secret);
// Build and sign a kind:1 text note.
const created_at = std.Io.Timestamp.now(io, .real).toSeconds();
const note = try nostr.event.create(
allocator, signer, keypair,
created_at, 1, &.{}, "hello from zig-nostr", null,
);
// Verify: recompute the canonical id, then check the Schnorr signature.
std.debug.assert(try nostr.event.verify(allocator, signer, note));
// Serialize to the wire JSON form.
const json = try nostr.event.toJson(allocator, note);
std.debug.print("{s}\n", .{json});
}The nostr module re-exports focused namespaces:
- Keys and encodings:
keys,hex,bech32,nip19,bip39,nip06,nip49. - Events and encryption:
event,filter,nip44,nip04,nip59,nip13. - Wire protocol and transport:
message,websocket,relay,liveness,nip42,nip65. - Storage and sync:
store,nip77. - Remote signing:
nip46,nip46_client,signer,keystore,signer_ipc. - HTTP codecs, with no transport:
nip05,nip11,nip98,nip86,blossom.
See the getting-started guide for more.
The last column says how much of each NIP is in the library, since several are codecs that leave the network to you. Everything listed is covered by tests; the cryptographic ones are verified against their official specification vectors, and negentropy is checked against transcripts recorded from the reference implementation.
| NIP | Title | What the library has |
|---|---|---|
| 01 | Basic protocol: events, signatures, subscriptions | Events, filters, and every client and relay message |
| 04 | Encrypted direct messages (legacy) | nip04: encrypt and decrypt, kept for interoperability with older clients |
| 05 | DNS-based identifiers | nip05: identifier parsing, the well-known URL and the response parser. You do the HTTP fetch |
| 06 | Key derivation from mnemonic seed | Derivation from a BIP-39 mnemonic |
| 09 | Event deletion | The store applies deletions on ingest |
| 11 | Relay information document | nip11: the URL, the Accept header and the parser. You do the HTTP fetch |
| 13 | Proof of work | nip13: difficulty, the committed target, and a multi-threaded miner |
| 17 | Private direct messages | nip59.privateDirectMessage builds the kind 14 rumor and its wraps; unwrap reads them |
| 19 | bech32-encoded entities | Encode and decode every entity |
| 21 | nostr: URI scheme |
Parse and strip, in nip19 |
| 42 | Client-to-relay authentication | The client's auth event |
| 44 | Encrypted payloads (v2) | Encrypt and decrypt, against the official vectors |
| 45 | Event counts | COUNT request and reply in message, both directions; Relay.count sends one |
| 46 | Nostr Connect, remote signing | Signer side (nip46, signer) and client side (nip46_client) |
| 49 | Private key encryption (ncryptsec) |
Encrypt and decrypt, scrypt and XChaCha20-Poly1305 |
| 50 | Search | The search filter field. Relays do the matching; the store matches a substring of the content |
| 59 | Gift wrap | nip59: rumor, seal, wrap and unwrap |
| 65 | Relay list metadata (outbox) | Parsing and read/write routing |
| 77 | Negentropy syncing | nip77: Negentropy v1, both roles, plus the NEG-* messages in message; Relay.negOpen, negMsg and negClose send them |
| 86 | Relay management API | nip86: request encoder, response parser and the NIP-98 header. You do the HTTP |
| 98 | HTTP auth | nip98: build and verify. You own the request and the clock |
| 10 | Reply threading | client-side |
Blossom (BUD-01, BUD-02, BUD-11) is in the library too, in blossom: the kind 24242 authorization token and the blob descriptor a server returns. No upload or download code is included.
NIP-10 is marked differently on purpose. There is no nip10 module here, because
threading is a decision about how to read e tags rather than a format to encode:
Plaza threads replies today and does it above the library. The same is true of
reactions, mentions and DNS identifiers, which is why they are not in this table.
It lists what you get by importing the library, not what an app on top of it can
do.
The library is proven by native apps built on it, the ecosystem forming around one core:
- Notary: shipped. A native remote
signer (NIP-46 bunker): your key lives in a local daemon, every signing
request waits for your approval, and the
nsecnever enters a client. Downloadable for macOS (Apple Silicon) and Linux (x86_64 and aarch64). - Plaza: shipped. The flagship: a fast, local-first client where you read without an account, post in four clicks, and the feed renders from disk. Downloadable for the same two.
Zaps and groups land inside Plaza. A messenger you have to switch to is one you stop using.
The library foundation has shipped: keys, transport, the local-first store and the signer protocol, built and tested on Linux, macOS and Windows. The apps and the command line are built on it, so what comes next for the library is how far it reaches:
- A C ABI over keys, events, NIP-19, NIP-44, relay reads and the local store, with a C header, and Swift bindings published as a Swift package. Both are tested in CI.
- An app engine extracted from Plaza: the parts with no pixels in them, which are sync, relay and outbox routing, signer sessions, caches for profiles, follow lists and relay lists, and content parsing into structured data. It has no views and no UI templates, so an app gets data and draws its own interface. Plaza moves onto it, and new small apps can be built on it, each with its own interface.
- An Android proof of concept that uses the library through the C ABI to read a feed into a local store on a phone. It is a proof of concept, not a store release.
Around those: Notary packaged for Windows, so the library, deed and Notary are each tested on Windows, macOS and Linux; deed mcp, which puts deed's verbs behind the Model Context Protocol so an agent can read relays, query the local store and verify events, with signing only through a NIP-46 signer that asks first; and Plaza 1.0.
Most of what remains is app work on protocol the library already exports: the HTTP transfer for Blossom uploads so a picture can be posted, NIP-57 zaps that can be sent rather than only verified, NIP-51 bookmarks, and NIP-29 groups. Reactions, notifications and the NIP-51 mute list are in Plaza already, and a repost can be sent, though one written by somebody you follow does not yet reach your feed. The NIP support page maps each one to the milestone that lands it.
The roadmap has the full list, and it also names what is deliberately not being built.
See CURRENT_STATE.md for exactly what's in progress, the
roadmap for the rest, and the
project board for the open work.
zig build # build the library
zig build test # run the test suite
zig build test -Dtest-filter=nip13 # run only tests whose name contains the text
zig fmt --check . # verify formattingThe Zig version is pinned in .zigversion. The library vendors
and compiles libsecp256k1 and LMDB from source, so no system packages are
required.
See CONTRIBUTING.md for the branch/PR/commit workflow and
AGENTS.md for a contributor- and agent-facing tour of the
codebase.
MIT, see LICENSE.