Skip to content
zig-nostrPublic

Repository files navigation

nostr

The Nostr protocol, natively in Zig.

CI License: MIT

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.

Highlights

  • πŸ”‘ 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.parseClientMessage and the relay-side encoders (OK, EOSE, CLOSED, NOTICE, AUTH, NIP-45 COUNT, NIP-77 NEG-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:// and nostrconnect://, 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.

How it is built

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.

Platforms

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

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=ReleaseFast

Methodology and the full write-up are on the benchmarks page.

Using it from an agent

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/nostr

For work from a shell rather than in Zig, deed is a command line built on this library, with a skill of its own.

Quickstart

Add the library to your build.zig.zon:

zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.16.0.tar.gz

Wire 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.

NIP support

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.

Built with it

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 nsec never 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.

Roadmap

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.

Development

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 formatting

The Zig version is pinned in .zigversion. The library vendors and compiles libsecp256k1 and LMDB from source, so no system packages are required.

Contributing

See CONTRIBUTING.md for the branch/PR/commit workflow and AGENTS.md for a contributor- and agent-facing tour of the codebase.

License

MIT, see LICENSE.

Releases

Packages

Contributors

Languages