# 11 — Nostrify: The Framework Layer

*The interface layer every MKStack app compiles against — one shape for storage (`NStore`) and relays (`NRelay`) means swapping in-memory for Postgres, or a browser extension for a bunker, is a config change, not a rewrite.*

**Verified: 2026-07-10**

Confidence:
- **High** — `NStore`/`NRelay`/`NostrSigner`/`NPolicy`/`NSchema`/`NUploader` interface shapes and the 9-package map (fetched directly from nostrify.dev and the live GitLab repo/API). Also high for the bus-factor figure — independently reproduced via the GitHub API, matching the maintainers' own same-day companion audit to the decimal.
- **Medium** — `NPool`'s `req()`-vs-`query()` EOSE/timeout split: documented clearly on one page, not cross-checked against the TypeScript source itself.
- **Medium** — `@nostrify/react`'s full hook surface: two hooks confirmed (`useNostr`, `useNostrLogin`); page content came back tool-summarized rather than read as raw markdown.
- **Low** — testing-utility completeness: `MockRelay`/`ErrorRelay`/`TestRelayServer` confirmed present in the live repo tree; a JSONL-fixture utility referenced in this manual's source research could not be independently relocated this pass.

---

## What It Is

Nostrify is Soapbox's "Framework for Nostr on Deno and web" [1] — the library every MKStack-scaffolded app imports for storage, relay access, signing, validation, moderation, and uploads. MIT-licensed, pnpm + Changesets monorepo [2][3], nine packages:

| Package | Ships |
|---|---|
| `@nostrify/nostrify` (core) | `NStore`, `NRelay`, `NRelay1`, `NPool`, `NostrSigner` + `NSecSigner`/`NConnectSigner`/`NBrowserSigner`, `NPolicy`, `NSchema`, `NUploader`, test utilities |
| `@nostrify/react` | React hooks + provider |
| `@nostrify/db` | `NPostgres`, `NDatabase` (Kysely SQL) |
| `@nostrify/indexeddb` | `NIndexedDB` (browser cache) |
| `@nostrify/strfry` | policy bridge into strfry's write-policy plugin interface |
| `@nostrify/policies` | 20 shipped `NPolicy` implementations (see below) |
| `@nostrify/ndk` | interop with NDK (Nostr Dev Kit) |
| `@nostrify/seed` | `NPhraseSigner`, `NSeedSigner`, `NCustodial` |
| `@nostrify/types` | shared TypeScript types |

Package list confirmed live against the monorepo's `packages/` directory [4]. `@nostrify/ndk` shows up only in nostrify.dev's integrations sidebar [5] and `@nostrify/types` only in the directory listing — neither package's own README was independently fetched this pass.

## One Interface, Many Backends

Two interfaces, one shape. **`NStore`** — `event()`, `query()`, `count()`, `remove()` [6]. **`NRelay`** — `req()`, `event()`, `query()` — and it's explicitly built on `NStore`: "Relays on Nostrify are actually just another type of storage. They implement `NRelay` (which is based on `NStore`)." [7] That sentence is the whole chapter in miniature: a relay, an in-memory cache, a Postgres table, and a browser's IndexedDB store are all the same shape to your app code.

| Interface | Implementation | Backend | Package |
|---|---|---|---|
| `NStore` | (in-memory) | process memory | core |
| `NStore` | `NDatabase` | any SQL via Kysely — SQLite, MySQL, etc. | db |
| `NStore` | `NPostgres` | Postgres, jsonb tags + NIP-50 full-text search, production-tested | db |
| `NStore` | `NIndexedDB` | browser IndexedDB, persistent client cache; degrades to a silent no-op under iOS Lockdown Mode | indexeddb |
| `NRelay` | `NRelay1` | one relay connection, auto-reconnecting | core |
| `NRelay` | `NPool` | many relays, outbox-model routing | core |

`NIndexedDB` additionally handles replaceable/addressable-event supersession and NIP-09 deletions, with configurable tag indexing [8]. `NPostgres` is named the production-tested adapter; `NDatabase` trades raw performance for broader SQL-engine compatibility [9]. "Storages can be used interchangeably with relays, allowing you to switch between in-memory, SQL databases, and more without changing your code" [6] — the same `query()` call an app makes against a live `NPool` in development can run against `NPostgres` in a worker, or `NIndexedDB` in an offline-first PWA, with zero call-site changes.

```mermaid
flowchart TB
    APP["Your app code\nstore.query(filters) / store.event(e)"]
    NSTORE["NStore interface\nevent · query · count · remove"]
    NRELAY["NRelay interface\nreq · event · query\n(built on NStore's shape)"]
    APP --> NSTORE
    APP --> NRELAY
    NSTORE --> MEM["in-memory\ncore"]
    NSTORE --> NDB["NDatabase\nany SQL via Kysely"]
    NSTORE --> NPG["NPostgres\njsonb tags + NIP-50 FTS"]
    NSTORE --> NIDB["NIndexedDB\nbrowser cache"]
    NRELAY --> NR1["NRelay1\none relay, auto-reconnect"]
    NRELAY --> NPOOL["NPool\nmany relays, outbox routing"]
    NPOOL --> RA["relay A"]
    NPOOL --> RB["relay B"]
    NPOOL --> RC["relay C\nNIP-65 write set"]
```

## NPool: Outbox Routing — and Its Timeout Trap

`NPool` is the `NRelay` implementation "designed with the Outbox model in mind" [10]. Its constructor takes two routing functions: a `reqRouter` mapping each relay URL to the filters it should serve (keyed on `filter.authors`), and an `eventRouter` mapping each outgoing event's `pubkey` to the relay URLs it should publish to. Feed it a user's NIP-65 relay list (kind 10002 — separate read/write sets) and this *is* outbox-model routing: the routing is the config, not a hardcoded relay array. A hardcoded relay list is the single most common reason "my app can't find this user's events" — it's routing to the wrong set, not a broken query.

EOSE behavior splits two ways depending which method you call, and this is the part that bites in production:

- **`pool.req()`** — the streaming subscription generator — "will only emit an EOSE when all relays in its set have emitted an EOSE" [10], and there is no timeout option on it at all. One dead or silent relay in the route means that subscription's EOSE signal never fires. Events from the healthy relays still stream through; the "stored events are exhausted" signal just never arrives.
- **`pool.query()`** — the one-shot, promise-returning path most application code actually calls — is safer by default: it "will wait up to 1 second after the first relay sends EOSE before canceling slow relays" [10], via an `eoseTimeout` option (default `1000`ms; set `0` to force wait-for-all).

`req()` does accept an `AbortSignal` [7] — that's the documented escape hatch, but `NPool` does not wire one up for you.

**Verdict:** don't trust `NPool`'s defaults for anything long-running. If your app calls `req()` directly for a live subscription — chat, a live feed, notifications — you own the timeout: wrap it in `AbortSignal.timeout(N)` or your own watchdog, because the default is "wait forever for the slowest relay in the set." `query()`'s 1-second default is workable for a UI-blocking fetch, but measure it against a cold `NPostgres`-backed relay before trusting it in a critical path.

## Signers: One Interface, Six Implementations

`NostrSigner` needs `getPublicKey()` and `signEvent()`, plus `nip04` and `nip44` sub-objects — each with `encrypt()`/`decrypt()` — built into the interface itself [11]. Every implementation gets encryption for free; no separate library, no separate call convention. The interface's whole reason for existing: "Signers from Nostrify are all drop-in replacements for `window.nostr`!" [11] — app code written against the browser-extension API swaps to any of the six with no call-site change.

| Class | Package | Backs onto |
|---|---|---|
| `NSecSigner` | core | raw nsec, held in-process |
| `NBrowserSigner` | core | wraps the browser's own `window.nostr` (NIP-07) |
| `NConnectSigner` | core | NIP-46 remote signer / bunker |
| `NPhraseSigner` | seed | BIP-39 mnemonic phrase |
| `NSeedSigner` | seed | raw HD seed |
| `NCustodial` | seed | custodial, server-held key |

Class names confirmed directly against the live repo file tree [12][13]; interface shape and the drop-in claim from the docs [11].

`NSecSigner` and `NCustodial` both hold key material in-process — the named catastrophic-failure shape the maintainers' companion audit has already flagged elsewhere (raw nsec in `localStorage` or a server env var) [24]. `NConnectSigner` is the one that keeps a brand or team key out of the app entirely; prefer it for anything beyond a disposable per-session identity.

## NSchema: Validate, Don't Assert

Zod-based; imported as `import { NSchema as n } from '@nostrify/nostrify'` — the `n` alias mirrors Zod's own `z` [14]. `n.event().parse(data)` validates a raw object into a well-formed event; chain `.refine(verifyEvent)` (from `nostr-tools`) to also check the signature. `n.filter()`, `n.relayMsg()`, `n.clientMsg()`, and `n.bech32('npub')` cover the rest of the wire format. Both `.parse()` (throws) and `.safeParse()` (returns a result object) ship on every schema [14] — the difference between "crash on bad relay data" and "handle it," and untrusted relay input is the normal case on this protocol, not the edge case.

## NPolicy: 20 Shipped, Not 19

`NPolicy` is one method — `call(event: NostrEvent): Promise<NostrRelayOK>`, `false` means reject [15]. The live repo tree (2026-07-10) ships 20 policy implementation files, each with a matching test file [16]; `nostrify.dev/policy/` currently documents 18 of them by name — `AuthorPolicy` and `ReplyBotPolicy` are shipped but not yet listed on the live docs page [15][16]. Same class of drift ch. 10 already caught between MKStack's README and its `package.json` [17] — trust the repo tree over the docs page.

| Policy | Does |
|---|---|
| `WoTPolicy` | Web-of-trust filtering — reject pubkeys outside a trust radius |
| `OpenAIPolicy` | AI-based content moderation via OpenAI's moderation API |
| `PowPolicy` | Requires NIP-13 proof-of-work difficulty — a spam deterrent |
| `PipePolicy` | Chains multiple policies into one pipeline |
| `HellthreadPolicy` | Caps mass-mention ("hellthread") events |

The same `NPolicy` object runs in two places unmodified: in a client, to reject-before-render, or piped into a relay via `@nostrify/strfry`, which "adapts Nostrify policies for use in strfry policy plugins" and "hooks up to stdin/stdout and runs the policy on strfry input" [18]. Write the moderation pipeline once; run it client-side today, relay-side later.

## Uploaders

`NUploader` is one method: `upload(file: File): Promise<[['url', string], ...string[][]]>` — the return is a NIP-94 tag array, first tag guaranteed to be `url` [19]. Two shipped implementations: `BlossomUploader` (one or more Blossom servers) and `NostrBuildUploader` (nostr.build or a compatible server) [19]. Output tags — `url`, `m` (MIME), `x` (sha256), `size`, `dim`, `blurhash` — drop directly into a kind 1063 (NIP-94) or inline (NIP-92) event with no reshaping.

## @nostrify/react

`NostrProvider` wraps the app in relay context; `useNostr()` returns the pool from any component; `useNostrLogin()` exposes login/logout state [20][21]. Under the hood the provider constructs an `NPool` over `NRelay1` connections [20] — this is the hook layer MKStack's own scaffold is built on (ch. 10 [17]). Pair `useNostr()`'s `.query()` calls with TanStack Query for caching; the docs' own example does exactly that [20].

## Testing Utilities — Repo-Only, Undocumented

The core package's `test/` directory ships `MockRelay.ts`, `ErrorRelay.ts`, and `TestRelayServer.ts` [12] — confirmed directly against the live file tree. None of the three appear anywhere on nostrify.dev; there is no dedicated testing page in its documented section list [1][12]. An app team that wants to write tests against `NPool` or `NRelay` today has to read source, not docs, to find them. A JSONL-fixture utility was flagged in this manual's source research but could not be independently relocated this pass — treat that specific claim as unconfirmed, not disproven.

## Longevity Note

Measured live via the GitHub API, 2026-07-10: of 1,043 total contributions to `soapbox-pub/nostrify`, `alexgleason` accounts for 889 — **85.2%** [22][23]. The next-largest contributor holds 141 (13.5%); five more combine for the remaining ~1.3% [23]. This reproduces, to the decimal, the figure the maintainers' own companion production-secrets-longevity audit measured earlier the same day [24] — two independent pulls, same number.

Nostrify is the single most load-bearing library in the stack — every MKStack app, and everything downstream of it, imports it directly. Sitting that on one person is a real risk, not a hypothetical one. The mitigation is available and cheap precisely because the license is permissive: MIT, confirmed on the root repo and every sub-package checked this pass [2][8][9][18]. Vendoring doesn't require anyone's permission. The maintainers' standing recommendation, unchanged by this pass: **vendor Nostrify source locally, and have one engineer read `NStore` and `NRelay` end-to-end** — so the interface contracts aren't being learned for the first time during an incident [24].

## Verdict

Nostrify earns the "framework" label the rest of this manual has used loosely. The interface discipline is real and live-verified: `NStore`/`NRelay` share one shape, every `NostrSigner` is a `window.nostr` drop-in, and one `NPolicy` object runs client-side or relay-side unchanged. Build against the interfaces, not a specific backend, and the "swap not rewrite" promise holds. Two traps: `NPool.req()` has no default timeout and will hang a subscription's EOSE forever on one bad relay — build the `AbortSignal` layer yourself, don't discover this live. And the whole framework sits at 85.2% single-author — MIT licensing makes vendoring cheap insurance; not vendoring it is the actual risk, not a hypothetical one.

## Sources

1. [nostrify.dev](https://nostrify.dev/) — homepage: tagline, framework description, documented section list (Relays/Storages/Signers/Schemas/Moderation Policies/Uploaders/Integrations — no testing section). Accessed 2026-07-10.
2. [gitlab.com/soapbox-pub/nostrify — README.md (raw)](https://gitlab.com/soapbox-pub/nostrify/-/raw/main/README.md) — monorepo description, pnpm + Changesets, MIT license. Accessed 2026-07-10.
3. GitLab API project metadata — `gitlab.com/api/v4/projects/soapbox-pub%2Fnostrify` — created 2024-01-16, 8 stars / 6 forks, last activity 2026-07-08. Accessed 2026-07-10.
4. GitLab API repository tree — `packages/` (depth 1) — confirms the 9-package list: db, indexeddb, ndk, nostrify, policies, react, seed, strfry, types. Accessed 2026-07-10.
5. [nostrify.dev/integrations/](https://nostrify.dev/integrations/) — integrations page: React, NDK, Zaps, MCP, Welshman listed; links to JSR API reference. Accessed 2026-07-10.
6. [nostrify.dev/store/](https://nostrify.dev/store/) — `NStore` interface (event/query/count/remove), implementation list, "interchangeable... without changing your code" line. Accessed 2026-07-10.
7. [nostrify.dev/relay/](https://nostrify.dev/relay/) — `NRelay`/`NRelay1` definitions, "based on NStore" quote, `req()`'s `AbortSignal` parameter. Accessed 2026-07-10.
8. [gitlab.com/soapbox-pub/nostrify — packages/indexeddb/README.md (raw)](https://gitlab.com/soapbox-pub/nostrify/-/raw/main/packages/indexeddb/README.md) — `NIndexedDB`: filter support, supersession/NIP-09 deletion handling, Lockdown Mode degradation to a silent no-op. Accessed 2026-07-10.
9. [gitlab.com/soapbox-pub/nostrify — packages/db/README.md (raw)](https://gitlab.com/soapbox-pub/nostrify/-/raw/main/packages/db/README.md) — `NPostgres` (jsonb + NIP-50 FTS, production-tested) vs `NDatabase` (Kysely, broad SQL compatibility). Accessed 2026-07-10.
10. [nostrify.dev/relay/pool](https://nostrify.dev/relay/pool) — `NPool` outbox routing (`reqRouter`/`eventRouter`), the `pool.req()` vs `pool.query()` EOSE/timeout split, `eoseTimeout` default and override. Accessed 2026-07-10.
11. [nostrify.dev/sign/](https://nostrify.dev/sign/) — `NostrSigner` interface, built-in `nip04`/`nip44`, "drop-in replacements for `window.nostr`" quote. Accessed 2026-07-10.
12. GitLab API repository tree — `packages/nostrify/` (recursive) — confirms `NSecSigner.ts`, `NConnectSigner.ts`, `NBrowserSigner.ts`, `NRelay1.ts`, `RelayError.ts`, and `test/MockRelay.ts` / `test/ErrorRelay.ts` / `test/TestRelayServer.ts`. Accessed 2026-07-10.
13. GitLab API repository tree — `packages/seed/` (recursive) — confirms `NCustodial.ts`, `NPhraseSigner.ts`, `NSeedSigner.ts`; no root README present. Accessed 2026-07-10.
14. [nostrify.dev/schema/](https://nostrify.dev/schema/) — `NSchema` (`NSchema as n`), Zod basis, `.parse()`/`.safeParse()`, event/filter/relayMsg/bech32 examples. Accessed 2026-07-10.
15. [nostrify.dev/policy/](https://nostrify.dev/policy/) — `NPolicy` interface (`call()` returning `NostrRelayOK`), 18 policies enumerated by name on this page. Accessed 2026-07-10.
16. GitLab API repository tree — `packages/policies/` (recursive) — confirms 20 policy implementation files incl. `AuthorPolicy.ts` and `ReplyBotPolicy.ts`, absent from the docs page. Accessed 2026-07-10.
17. This manual, [ch. 10 — MKStack](10-mkstack.md) — cross-referenced for the README-vs-manifest drift pattern (reused here for docs-page-vs-repo-tree) and for `@nostrify/react`'s role as MKStack's hook layer.
18. [gitlab.com/soapbox-pub/nostrify — packages/strfry/README.md (raw)](https://gitlab.com/soapbox-pub/nostrify/-/raw/main/packages/strfry/README.md) — the strfry write-policy bridge, stdin/stdout adapter, MIT license reconfirmed. Accessed 2026-07-10.
19. [nostrify.dev/upload/](https://nostrify.dev/upload/) — `NUploader` interface, `BlossomUploader`/`NostrBuildUploader`, NIP-94 tag output shape. Accessed 2026-07-10.
20. [nostrify.dev/react/](https://nostrify.dev/react/) — `NostrProvider`, `useNostr()`, setup pattern, TanStack Query pairing example. Accessed 2026-07-10.
21. [gitlab.com/soapbox-pub/nostrify — packages/react/README.md (raw)](https://gitlab.com/soapbox-pub/nostrify/-/raw/main/packages/react/README.md) — cross-check: `NostrContext.Provider`, `NostrLoginProvider`, `useNostrLogin()`. Accessed 2026-07-10.
22. [github.com/soapbox-pub/nostrify](https://github.com/soapbox-pub/nostrify) — confirms a live GitHub mirror exists (unlike MKStack, ch. 10): 1,050 commits, 17 stars, MIT. Accessed 2026-07-10.
23. GitHub API — `api.github.com/repos/soapbox-pub/nostrify/contributors` — full contributor list: alexgleason 889, xyzshantaram 141, patrickReiis 6, hzrd149 2, sergey3bv 2, marykatefain 1, dentropy 1, Sjors 1 (1,043 total). Accessed 2026-07-10.
24. Maintainers' internal production-secrets-longevity audit note (companion document, not part of this public chapter set) — same-day bus-factor measurement (Nostrify 85.2% Gleason, independently cross-checked here) and the standing pre-positioning recommendation to vendor Nostrify source + read `NStore`/`NRelay` end-to-end.
