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.3.8). 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.
- 🔒 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.
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.
Add the library to your build.zig.zon:
zig fetch --save https://github.com/zig-nostr/nostr/archive/refs/tags/v0.3.8.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();
// 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 note = try nostr.event.create(
allocator, signer, keypair,
std.time.timestamp(), 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, event,
nip19/bech32, nip06/bip39, nip49, nip44, nip46, nip42,
relay/websocket/message/filter, nip65, and store. See the
getting-started guide for more.
Every "done" NIP is covered by tests; the cryptographic ones are verified against their official specification vectors.
| NIP | Title | Status |
|---|---|---|
| 01 | Basic protocol: events, signatures, subscriptions | ✅ |
| 06 | Key derivation from mnemonic seed | ✅ |
| 09 | Event deletion (ingestion) | ✅ |
| 19 | bech32-encoded entities | ✅ |
| 21 | nostr: URI scheme |
✅ |
| 42 | Client-to-relay authentication | ✅ |
| 44 | Encrypted payloads (v2) | ✅ |
| 46 | Nostr Connect, remote signing | ✅ |
| 49 | Private key encryption (ncryptsec) |
✅ |
| 65 | Relay list metadata (outbox) | ✅ |
| 17 | Private direct messages | 🚧 planned |
| 59 | Gift wrap | 🚧 planned |
| 10 | Reply threading | client-side |
NIP-17 and NIP-59 arrive together, in Plaza's private-messages milestone.
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 macOS
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. - 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. A downloadable macOS app.
Private messages, zaps and groups land inside Plaza rather than as separate apps. 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. Both apps are downloadable and both keep growing, so what comes next lands inside them rather than as new apps.
In spec terms, the library still owes NIP-17 private direct messages, with the
NIP-59 gift wrap that carries them, and a search field on Filter for NIP-50.
Most of what remains is app work on protocol the library already exports:
reactions, reposts and zap receipts behind notifications, NIP-51 bookmarks and
mutes, Blossom uploads so a picture can be posted, NIP-57 zaps paid through a
NIP-47 wallet, and NIP-29 groups. The NIP support
page maps each one to the milestone that lands it.
There are no dates. The roadmap is an order, and it also names what is deliberately not being built (set-reconciliation sync, NIP-77, is on that list), which is the half most roadmaps leave out.
See CURRENT_STATE.md for exactly what's in progress, the
roadmap for the sequence, and the
project board for the milestone
tracker.
zig build # build the library
zig build test # run the test suite
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.