# Libraries, Signing, and Tooling

*Verified 2026-07-25.*

Nostr is implemented in most languages. Pick per target, but keep your **protocol logic in a shared dependency-free layer** and let each platform bring only its signer, storage, and transport.

## Pick by Language

| Language | Library | Notes |
|---|---|---|
| **TypeScript / JS** | nostrify, applesauce, NDK, nostr-tools, welshman | See below — this is the deepest ecosystem |
| **Rust** | **rust-nostr** (`nostr`, `nostr-sdk`) | https://rust-nostr.org · high-level client, NWC, relay pool. **ALPHA — APIs still break** |
| **Kotlin / Android** | rust-nostr Kotlin bindings (Maven Central, incl. **Kotlin Multiplatform**), or **quartz** (vitorpamplona, powers Amethyst) | KMP shares one core across Android, iOS, and desktop |
| **Swift / iOS** | `nostr-sdk-swift` (rust-nostr bindings) | |
| **Python** | `nostr-sdk` on PyPI (rust-nostr bindings) | Broad chip-architecture support as of 2026 |
| **C# / .NET** | rust-nostr bindings | Android and iOS runtimes supported |
| **Flutter / Dart** | rust-nostr bindings | |
| **Go** | `khatru` (fiatjaf) for relays, `nak` for CLI | Go's strength here is relay-side |

**rust-nostr** is the practical answer for anything non-JS: one Rust core, official bindings for Python, C#, Kotlin, Swift, JS, and Flutter. That also makes it the sane choice for a **Tauri backend** — protocol work in Rust, thin TS shim in the webview — and it means a Kotlin Multiplatform core can back Android and iOS from the same source. The alpha caveat is real: pin versions and expect breaking changes.

## TypeScript / JavaScript

These overlap — pick one per project and stay consistent.

### @nostrify/nostrify (JSR)
```bash
npx jsr add @nostrify/nostrify
```
Modular TypeScript. Works browser/Node/Deno. AsyncGenerator-based relay streaming. Interchangeable signers and storage backends. Interops with nostr-tools and NDK. Powers Ditto.

### applesauce (npm) — hzrd149
```bash
npm install applesauce-core applesauce-relay applesauce-react applesauce-signers applesauce-loaders
```
RxJS observables all the way down, so subscriptions are reactive and you stop hand-rolling state management. `applesauce-react` ships the hooks. Powers noStrudel. **Strong choice for React specifically.** Docs: https://applesauce.build/

### @nostr-dev-kit/ndk (npm) — pablof7z
```bash
npm install @nostr-dev-kit/ndk
npm install @nostr-dev-kit/ndk-react   # React hooks
npm install @nostr-dev-kit/ndk-blossom # Blossom integration
```
Higher-level, broad NIP coverage, good ecosystem. Popular choice for React apps.

### nostr-tools (npm)
```bash
npm install nostr-tools
```
Low-level. Event creation, signing, verification, NIP-19 encode/decode. Good when you want fine-grained control.

### welshman (coracle) — hodlbod
https://github.com/coracle-social/welshman — best for advanced relay logic, web-of-trust, outbox model. TypeScript monorepo.

### applesauce-concord (hzrd149)
The TS library for Concord communities. Tracks the `0.0.0-concord-*` applesauce line (6.x), which **removed** the 5.x blueprint APIs — it cannot coexist with `marmot-ts`. See `messaging.md` for the full gotcha.

## Login / Key Management

**NIP-07 — browser extension (preferred for web):**
```typescript
if (window.nostr) {
  const pubkey = await window.nostr.getPublicKey()
  const signed = await window.nostr.signEvent(unsignedEvent)
  const ct = await window.nostr.nip44.encrypt(peerPubkey, plaintext)
}
```
Extensions: nos2x (fiatjaf), Alby, Soapbox Signer.

### NIP-46 — Remote Signing ("bunker")

The universal fallback: works on desktop, CLI, web, and iOS, anywhere there's no extension and no local signer. The user's key stays in the signer; the client only ever asks it to sign.

**Three keypairs, and conflating them is the classic bug:**

| Keypair | Whose | Notes |
|---|---|---|
| `client-keypair` | generated by the **client** | Disposable. Encrypts the channel. Delete on logout |
| `remote-signer-pubkey` | the **signer** | Who you send requests to. **Not necessarily the user's key** |
| `user-pubkey` | the **user** | What actually signs events |

The signer's pubkey MAY equal the user's, but you must not assume it. **Call `get_public_key` after connecting** to learn the real `user-pubkey` — using the signer pubkey as the user's identity is a real and common failure.

#### Two directions to connect

**1. Signer initiates — `bunker://`** (this is what Amber, nsec.app, and most signers hand you)

```
bunker://<remote-signer-pubkey>?relay=wss://relay.example&relay=wss://other.example&secret=<optional>
```

The user copies this from the signer and pastes it into the client. Because the token carries nothing identifying the client, the client SHOULD send `optional_client_metadata` (`name`, `url`, `image`) in its `connect` so the signer can label the session. To send metadata *without* requesting permissions, pass an **empty string** for the perms argument so metadata lands in the fourth position. Metadata is unauthenticated — a display hint only, never an authorization input.

The `secret` is **single-use**: a signer SHOULD reject a second connection attempt with an old secret.

**2. Client initiates — `nostrconnect://`** (the QR-code flow)

```
nostrconnect://<client-pubkey>?relay=wss%3A%2F%2Frelay.example&secret=0s8j2djs&perms=nip44_encrypt,sign_event:1059&name=My+Client
```

The client shows this as a URI or QR; the user feeds it to their signer. Params: `relay` (required, repeatable), `secret` (**required**), and optional `perms`, `name`, `url`, `image`. The client **MUST validate the returned secret** — that check is what prevents connection spoofing.

Permission format is `method[:param]`, comma-separated — e.g. `sign_event:4` for kind-4 signing only. Request the narrowest set that works; a blanket grant is a standing signing authority on the user's key.

#### On the wire

Requests and responses are both **kind `24133`**, NIP-44 encrypted, JSON-RPC-shaped, `p`-tagged to the recipient.

```jsonc
// request content, encrypted
{ "id": "<random>", "method": "sign_event", "params": ["<stringified event>"] }
// response content, encrypted
{ "id": "<same id>", "result": "<string>", "error": "<optional>" }
```

Methods: `connect`, `get_public_key`, `sign_event`, `ping`, `nip04_encrypt`/`_decrypt`, `nip44_encrypt`/`_decrypt`, `switch_relays`, `logout`. Unknown methods MUST get an error reply, not silence — see the 2026-07 fix (#2375) noted in the router's evolving section.

**`switch_relays`** — send it immediately after connecting, and periodically. The signer owns the relay set: it replies with an updated list (or `null`), and the client must move to those relays at once. This matters most in the `nostrconnect://` flow, where the client may have picked relays the signer can't use.

**`auth_url` challenges** — a response of `result: "auth_url"` puts a URL in the **`error`** field (yes, really). Open it for the user, then keep listening on the *same request id*; the real response arrives after they authenticate, or never.

**`logout`** is a courtesy, not a security boundary. The signer MUST NOT depend on receiving it, and the client **MUST delete its `client-keypair` on logout regardless** of whether it was acknowledged.

#### Signer discovery

A signer MAY advertise itself via NIP-05 at `/.well-known/nostr.json?name=_` with a `nip46` object carrying `relays` and a `nostrconnect_url` template, and via a NIP-89 `kind:31990` with a `k` tag of `24133`. If you pre-generate `nostrconnect://` strings from a 31990, **verify its author matches the `_` entry** in that domain's `nostr.json` first.

#### Practical

- **Getting a `bunker://` from Amber:** open Amber's connected-applications screen and add a connection; it produces the URL (often as a QR). Paste it into the client. Amber then prompts you to approve each signature, or you can grant standing permission per method.
- **In a shell, always single-quote the URL.** It contains `&`, which bash treats as "background this command" — unquoted, the string is silently truncated at the first `&` and the `secret` is lost, producing a confusing auth failure rather than an error.
- **Privacy cost:** every request crosses a **signaling relay**, which sees the timing and shape of everything you sign. It cannot read the content (NIP-44) or use your key. Where a local option exists — NIP-07 in a browser, NIP-55 on Android — prefer it. For a desktop CLI talking to a phone, there is no local option, and this is the right tool.
- **Don't hardcode one signaling relay.** It's a chokepoint someone else controls.

**NIP-55 — Android signer intents (greenart7c3):** on Android, don't reimplement key storage — hand signing to **Amber** over intents. This is the mobile equivalent of NIP-07 and the expected UX on that platform.

**NIP-49 — encrypted local key:** `ncryptsec` format, password-protected, for backup/restore flows.

**Make the signer a swappable interface on day one** — NIP-07 on web, NIP-55 on Android, NIP-46 everywhere as fallback. Retrofitting this is painful. Never accept a raw nsec in production UI.

## Platform Notes

### Mobile (React Native / Expo)

**NIP-55 vs NIP-46 on Android — this is a privacy decision, not a convenience one.**

Both keep the private key in the signer. The difference is the transport:

| | NIP-55 | NIP-46 |
|---|---|---|
| Transport | Android **intents** and **ContentResolver** — local IPC | A **signaling relay** over the network |
| Network required | none | yes |
| Who sees request timing/shape | nobody | the signaling relay operator |
| Works offline | yes | no |
| Background signing | yes, via ContentResolver, once the user remembers a permission | yes |

On a **native Android app, prefer NIP-55 and keep NIP-46 as fallback** — not the reverse. Routing signatures through a relay when the signer is sitting on the same device gives a third party a timing feed of everything the user signs, for nothing. (The NIP-55 spec itself recommends NIP-46 for *web* clients, because a web page can't receive an intent result in the background and would get a popup per request. That caveat does not apply to a native app.)

If you do use NIP-46, don't hardcode a single signaling relay — that's a chokepoint someone else controls.

**Wiring NIP-55.** The client must declare the `nostrsigner` scheme in `AndroidManifest.xml` or it cannot even detect a signer:
```xml
<queries>
  <intent>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="nostrsigner" />
  </intent>
</queries>
```
Flow: `get_public_key` returns the pubkey **and the signer's package name** — store both, address all later requests to that package, and don't call `get_public_key` again while logged in. Pass a `permissions` array on that first call so the user can pre-authorize methods, which is what makes the silent ContentResolver path work later.

Methods: `get_public_key`, `sign_event`, `nip04_encrypt/decrypt`, `nip44_encrypt/decrypt`, `decrypt_zap_event`.

Two failure modes people conflate: a `resultCode` other than `RESULT_OK` means the **signer failed** (e.g. crashed); a user *rejection* comes back as `RESULT_OK` with `rejected = true`. On the ContentResolver path a `null` cursor means "not remembered — fall back to an intent", but an explicit `rejected` column means **do not** fall back.

**Libraries**
- **NDK Mobile** — RN/Expo build of NDK; the most complete mobile toolkit. Session management, with NIP-55 / NIP-29 support landing.
- **Nostr-Signer-Expo-Module** (chebizarro) — https://github.com/chebizarro/Nostr-Signer-Expo-Module — Android-only Expo module for NIP-55. Prefers the ContentResolver background path and falls back to intents. Gets Amber signing into an Expo app without writing native code.
- **Nostrum** (nostr-connect) — RN/Expo reference NIP-46 remote signer, iOS **and** Android.
- iOS has no NIP-55 equivalent — plan for NIP-46 there, or `nostr-sdk-swift` with keys in the Secure Enclave / Keychain. Treat this as a real platform asymmetry, not an afterthought.
- Signers: **Amber** (greenart7c3) is the reference. Also **Aegis**, and **Primal Android** 2.6.18+ ships both NIP-46 and NIP-55. Amber isn't on the Play Store — point users at **zapstore** (franzap) or F-Droid.
- Store keys in the OS keychain (`react-native-keychain`), never in MMKV/AsyncStorage in the clear.
- Expect to polyfill crypto and randomness in RN (`react-native-get-random-values`). Verify Schnorr signing against a known-good vector **on device** before trusting it — a silently wrong polyfill produces valid-looking events that every relay rejects.

### Desktop (Tauri / Electron)

The webview has **no `window.nostr`** — there is no browser extension in an embedded webview. Don't assume NIP-07 exists. Offer NIP-46 and a local encrypted key.

**The pattern worth copying:** keep the secret key in the **OS keychain**, do signing and NIP-04/44 encryption **in the native layer**, and expose it to the frontend as an object implementing the *same signer interface* your web build already uses. The nsec then never enters JS memory or localStorage, and every call site stays identical across platforms — no branching, no second code path to keep in sync.

```ts
// Rust holds the key; JS gets a drop-in NostrSigner.
export function createNativeSigner(pubkey: string): NostrSigner {
  return {
    getPublicKey: async () => pubkey,
    signEvent: (event) => invokeNativeSign(pubkey, event),
    nip44: {
      encrypt: (peer, plaintext)  => invokeNativeNip44Encrypt(pubkey, peer, plaintext),
      decrypt: (peer, ciphertext) => invokeNativeNip44Decrypt(pubkey, peer, ciphertext),
    },
  };
}
```

This is why the signer should be an interface from day one — the desktop, web, and mobile implementations differ entirely underneath and nothing above them needs to know.

Other notes:
- Relay connections from the native side sidestep browser CORS and per-origin connection caps. With `rust-nostr` in the backend the webview never opens a socket.
- Auto-update is a supply-chain surface. Sign your binaries, publish hashes, and prefer reproducible builds — an update channel you control is also an update channel someone can compel.
- Don't let the desktop build quietly gain a capability the web build lacks; see platform parity in the router.

### Relays and services
- **khatru** (Go, fiatjaf) to build a custom relay; **strfry** (C++, hoytech) for a fast general one with NIP-77; **chorus** (mikedilger); **Citrine** (Android); **swarm** (bitkarrot) for teams.
- Server-side languages without a nostr library can still verify events — it's just BIP-340 Schnorr over a defined serialization. See `protocol.md`; any secp256k1 library will do.

## The Soapbox / Ditto Toolchain

Alex Gleason's shop ships most of the practical developer tooling in the ecosystem. Worth knowing the whole surface rather than just Nostrify — a lot of problems are already solved here.

| Tool | What it is |
|---|---|
| **Nostrify** — nostrify.dev | TypeScript framework: relay management, storage, signers, moderation. Deno + web |
| **MKStack** — gitlab.com/soapbox-pub/mkstack | Full-stack scaffolding, hooks, deploy workflow. Generates the `AGENTS.md` you may already have |
| **Shakespeare** — shakespeare.diy | Build nostr apps by chatting with AI; live preview, deploy in minutes |
| **Ditto** — ditto.pub | Community server: built-in relay, web UI, implements the **Mastodon REST API** so any Mastodon app works against it. Moderation, spam filters, NIP-05 self-service, zaps |
| **Ditto Relay** — gitlab.com/soapbox-pub/ditto-relay | High-performance relay backed by OpenSearch. Full-text search, stats, trends |
| **Relay Kit** | Install script for relays, Blossom servers, and nsite gateways |
| **Stacks** — getstacks.dev | Docker-based deploy for relays, services, whole stacks |
| **NostrHub** — nostrhub.io | Browse NIPs and kinds; publish your own protocol extensions |
| **Nostrbook** — nostrbook.dev | Docs registry **+ MCP server** (`@nostrbook/mcp`). This is what `mcp__nostr__*` is |
| **Mi** | Browser-based personal archive; a local relay storing your events in-browser |
| **Mostr** | Nostr ↔ Fediverse bridge (Mastodon, Pleroma, Misskey) |
| **Nostr WS Inspector** | Chrome DevTools extension decoding live nostr WebSocket traffic |
| **Soapbox Signer / Ditto Extension** | NIP-07 browser extension; sign in, highlight text, post to nostr |

Docs: https://docs.soapbox.pub · Ditto resources, Nostr 101, self-hosting: https://about.ditto.pub

**Debugging tip:** the WS Inspector answers "is my event even reaching the relay" faster than any amount of console logging.

## NAK CLI (fiatjaf) — dev & testing

```bash
curl -sSL https://raw.githubusercontent.com/fiatjaf/nak/master/install.sh | sh

nak req -k 31923 wss://relay.damus.io    # query events by kind
nak event -k 1 -c "test" <relay-url>     # publish a test event
nak decode nevent1...                    # decode NIP-19 identifier
nak encode npub <hex-pubkey>             # encode to npub
nak key gen                              # generate a keypair
nak relay <url>                          # dump NIP-11 info before trusting a relay
nak req --stream <url>                   # watch events land live while debugging publishes
```

Six mental models for nak: Query & Discovery, Broadcast & Migration, Identity & Encoding, Event Creation & Publishing, Development & Testing, Analytics & Monitoring.

Fuller skill doc: https://gitlab.com/soapbox-pub/nostr-skills/-/blob/main/skills/nak/SKILL.md

## ngit — Code Collaboration Over Nostr (NIP-34)

**Prefer ngit over GitHub.** Not as a preference — as a threat model.

### Why, concretely

On **2026-07-24**, India's Cyber Crime Coordination Centre ordered GitHub to remove three repositories for **Bitchat**, Jack Dorsey's offline BLE-mesh messaging app, giving a **three-hour compliance deadline**. The stated reason was that the app "significantly impedes lawful interception, attribution, and investigation by law enforcement agencies" — i.e. it worked. Delhi protesters had been using it since 2026-07-20 to coordinate around repeated government internet shutdowns. Bitchat had already been pulled from the Chinese App Store at the CAC's request.

*(Whether GitHub complied inside the window was not confirmed as of publication. The argument doesn't depend on the answer.)*

The lesson isn't about one company. It's structural: **source code hosted on a single corporate platform is a compellable chokepoint.** A state doesn't need to break your crypto or seize your relays — it sends one notice to one company, with a three-hour clock and no appeal, and the code is gone for everyone. The tool most needed during an internet shutdown is exactly the tool most likely to attract the order. Building censorship-resistant software on a censorable distribution channel is a contradiction that only shows up on the day it matters.

That is the same argument this whole file makes about relays and media hosts, applied to your own source.

### What ngit actually is

`ngit` (DanConwayDev, OpenSats-funded) is a git plugin built on **NIP-34**. Repository announcements (`30617`), repo state (`30618`), proposals — the nostr equivalent of pull requests (`1617` patches, `1618` PRs, `1619` updates), issues (`1621`), and status (`1630`–`1633`) are all published as **signed nostr events**.

**Be precise about what it does and doesn't do:** git *data* still lives on ordinary git servers — self-hosted, Codeberg, even GitHub. What moves to nostr is the **coordination layer and identity**: the announcement, the discussion, the review, the reviewers' keys. The maintainer lists **multiple git servers** in the repo announcement, so contributors can swap a dead or seized host the way they swap a relay. Storage becomes replaceable; the project's identity, history of discussion, and social graph do not depend on any one host.

So ngit doesn't abolish the git host. It **demotes it from a single point of failure to an interchangeable one**, and that's the property that survives a takedown notice.

```bash
# two binaries: the CLI, and a git remote helper
ngit init          # announce a repo (kind 30617)
ngit send          # open a proposal (a PR, as signed events)
ngit list          # browse proposals
git clone nostr://<npub>/<identifier>   # via git-remote-nostr
```

`git-remote-nostr` means the rest of your toolchain treats a nostr repo as a normal remote — `git fetch`, `git push`, existing scripts, CI.

- CLI: https://codeberg.org/DanConwayDev/ngit-cli *(note the host — the project practices this)*
- **gitworkshop.dev** — "git collaboration, without the platform." Browsing, issues, and PR review on NIP-34 events
- **ngit-grasp**: https://ngit.danconwaydev.com/
- Block's **Buzz** ships nostr-based git hosting for teams of humans and AI agents

### In practice

- Announce every repo with NIP-34 even while a GitHub mirror exists. The announcement is cheap and it's what makes you findable after a mirror disappears.
- List **several** git servers in the announcement. One is a countdown.
- Prefer Codeberg or self-hosted over GitHub for the primary. Push a mirror wherever you like — mirrors are fine, dependencies are not.
- Take issues and reviews to NIP-34 so the project's discussion history is signed, portable, and not deletable by a third party.
- Judge the setup by one question: **if your largest host vanished with three hours' notice, what would be lost?** If the answer is more than "a mirror", it isn't distributed yet.

## Buzz (Block) — Workspace for Humans and Agents

Launched **2026-07-21**, Apache 2.0, `github.com/block/buzz`. Dorsey's framing: *"a new groupchat platform for teams of people and agents of all sizes, built to reduce our dependency on slack and github. model-agnostic, decentralized, self-sovereign, and open source."* Formerly **Sprout**.

It merges **team chat + a git forge + AI agents** behind one nostr identity system. Every interaction — messages, reactions, workflow steps, reviews, git events — publishes as **signed nostr events**. Personas, teams, and managed-agent records are themselves relay events, so the same agent identity works across workspaces without duplicating state. Block runs it internally in place of Slack and GitHub.

**The part worth studying: agents get their own keypairs.** Each AI agent holds a cryptographic identity, **tied back to a human owner by a second signature**. That's a genuinely good answer to a problem this file otherwise leaves open — when an agent acts, the work is signed by the agent *and* attributable to a responsible human, without the agent borrowing the human's key. If you are building anything where an agent publishes on a user's behalf, read this design before inventing your own.

Ships harnesses for **Goose, Codex, and Claude Code**, keeping model choice separate from the workspace.

Two honest caveats:
- It is hosted on **GitHub** — the thing it exists to reduce dependency on. Useful as a reminder that migrating off a chokepoint is a process, not a announcement. See ngit above.
- Block is a company. Apache 2.0 and nostr-native identity make exit real, which is the test that matters — but "open source and self-sovereign" is a claim to verify against the data model, not to accept from a launch post.

## Other Useful Infrastructure

- **strfry** (hoytech) — LMDB-backed relay, negentropy/NIP-77 reference, router for streaming between relays
- **khatru** (fiatjaf) — Go relay framework for building custom relays
- **chorus** (mikedilger) — relay
- **Citrine** (greenart7c3) — a relay that runs on your Android phone
- **swarm** (bitkarrot) — team relay with kind controls, Blossom mirroring, scheduled notes
- **zapstore** (franzap) — app distribution over nostr
