{"id":20508644,"url":"https://github.com/soapbox-pub/nostrify","last_synced_at":"2025-08-12T21:12:01.257Z","repository":{"id":239628532,"uuid":"798041634","full_name":"soapbox-pub/nostrify","owner":"soapbox-pub","description":"Bring your projects to life on Nostr. 🌱 This repo is a mirror of the source on GitLab.","archived":false,"fork":false,"pushed_at":"2025-05-27T09:04:43.000Z","size":4847,"stargazers_count":10,"open_issues_count":4,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-27T09:35:14.740Z","etag":null,"topics":["deno","nostr"],"latest_commit_sha":null,"homepage":"https://nostrify.dev/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soapbox-pub.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-05-09T01:22:55.000Z","updated_at":"2025-05-27T09:04:47.000Z","dependencies_parsed_at":"2024-08-16T03:20:51.418Z","dependency_job_id":"efece708-39df-474f-9dd4-f3d4bf58354a","html_url":"https://github.com/soapbox-pub/nostrify","commit_stats":null,"previous_names":["soapbox-pub/nostrify"],"tags_count":58,"template":false,"template_full_name":null,"purl":"pkg:github/soapbox-pub/nostrify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soapbox-pub%2Fnostrify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soapbox-pub%2Fnostrify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soapbox-pub%2Fnostrify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soapbox-pub%2Fnostrify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soapbox-pub","download_url":"https://codeload.github.com/soapbox-pub/nostrify/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soapbox-pub%2Fnostrify/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262647073,"owners_count":23342579,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deno","nostr"],"created_at":"2024-11-15T20:19:36.400Z","updated_at":"2025-06-29T18:32:22.902Z","avatar_url":"https://github.com/soapbox-pub.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nostrify\n\nBring your projects to life on Nostr. 🌱\n\nNostrify is a Nostr framework for web browsers and Deno. It's made up of of simple modules that can be used independently, or swapped out with your own implementations.\n\nUse it alongside nostr-tools, NDK, or your existing application. Nostrify can be gradually adopted and plays nice with others.\n\n## Schema\n\nA suite of [zod](https://github.com/colinhacks/zod) schemas for Nostr are available in the `NSchema` module.\n\n```ts\nimport { NSchema as n } from '@nostrify/nostrify';\n\nconst event: NostrEvent = n.event().parse(eventData);\nconst metadata: NostrMetadata = n.json().pipe(n.metadata()).parse(event.content);\nconst msg: NostrRelayMsg = n.relayMsg().parse(e.data);\nconst nsec: `nsec1${string}` = n.bech32('nsec').parse(token);\n```\n\n## Storages\n\nStorages (implementing the `NStore` interface) allow interacting with Nostr events.\nA database is a Storage. A relay is a Storage. A cache is a Storage.\nIt should be possible to use Nostr storages interchangeably to get the best performance.\n\n### `NStore` interface\n\n`NStore` is the interface that all Nostr Storages implement.\n\n```ts\n/** Nostr event store. */\ninterface NStore {\n  /** Add an event to the store (equivalent of `EVENT` verb). */\n  event(event: NostrEvent, opts?: NStoreOpts): Promise\u003cvoid\u003e;\n  /** Get an array of events matching filters. */\n  query(filters: NostrFilter[], opts?: NStoreOpts): Promise\u003cNostrEvent[]\u003e;\n  /** Get the number of events matching filters (equivalent of `COUNT` verb). */\n  count?(filters: NostrFilter[], opts?: NStoreOpts): Promise\u003cNostrRelayCOUNT[2]\u003e;\n  /** Remove events from the store. This action is temporary, unless a kind `5` deletion is issued. */\n  remove?(filters: NostrFilter[], opts?: NStoreOpts): Promise\u003cvoid\u003e;\n}\n```\n\n### `NCache` class\n\nNostr LRU cache based on [`npm:lru-cache`](https://www.npmjs.com/package/lru-cache).\nIt implements both `NStore` and `NSet` interfaces.\n\n```ts\n// Accepts the options of `npm:lru-cache`:\nconst cache = new NCache({ max: 1000 });\n\n// Events can be added like a regular `Set`:\ncache.add(event1);\ncache.add(event2);\n\n// Can be queried like `NStore`:\nconst events = await cache.query([{ kinds: [1] }]);\n\n// Can be iterated like `NSet`:\nfor (const event of cache) {\n  console.log(event);\n}\n```\n\n### `NDatabase` class\n\nSQLite database storage adapter for Nostr events.\nIt uses [Kysely](https://kysely.dev/) to make queries, making it flexible for a variety of use-cases.\n\n```ts\n// Create a Kysely instance.\nconst kysely = new Kysely({\n  dialect: new DenoSqliteDialect({\n    database: new Sqlite('./db.sqlite3'),\n  }),\n});\n\n// Pass Kysely into the constructor.\nconst db = new NDatabase(kysely);\n\n// Migrate the database before use.\nawait db.migrate();\n\n// Now it's just a regular storage.\nawait db.event(event1);\nawait db.event(event2);\nconst events = await db.query([{ kinds: [1] }]);\n```\n\n### `NSet` class (not really a storage)\n\nNostr event implementation of the `Set` interface.\n\nNSet is an implementation of the theory that a Nostr Storage is actually just a Set.\nEvents are Nostr's only data type, and they are immutable, making the Set interface ideal.\n\n```ts\nconst events = new NSet();\n\n// Events can be added like a regular `Set`:\nevents.add(event1);\nevents.add(event2);\n\n// Can be iterated:\nfor (const event of events) {\n  if (matchFilters(filters, event)) {\n    console.log(event);\n  }\n}\n```\n\n`NSet` will handle kind `5` deletions, removing events from the set.\nReplaceable (and parameterized) events will keep only the newest version.\nHowever, verification of `id` and `sig` is NOT performed.\n\nAny `Map` instance can be passed into `new NSet()`, making it compatible with\n[lru-cache](https://www.npmjs.com/package/lru-cache), among others.\n\n## Relays\n\nRelays are an extended form of Storage with real-time streaming capabilities.\n\n### `NRelay` interface\n\n`NRelay` implements all the methods of `NStore`, including a `req` method for streaming events.\n\n```ts\ninterface NRelay extends NStore {\n  /** Subscribe to events matching the given filters. Returns an iterator of raw NIP-01 relay messages. */\n  req(filters: NostrFilter[], opts?: NReqOpts): AsyncIterable\u003cNostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED\u003e;\n}\n```\n\nThe `req` method returns raw NIP-01 relay messages, but only those pertaining to subscriptions: `EVENT`, `EOSE`, and `CLOSED`.\n\nOther messages such as `COUNT` and `OK` are handled internally by `NStore` methods:\n\n- `NRelay.event` - sends an `EVENT` and waits for an `OK`. If the `OK` is false, an error is thrown with the reason as its message.\n- `NRelay.query` - calls `NRelay.req` internally, closing the subscription automatically on `EOSE`.\n- `NRelay.count` - sends a `COUNT` and waits for the response `COUNT`.\n- `NRelay.remove` - not applicable.\n\nOther notes:\n\n- `AUTH` is not part of the interface, and should be handled by the implementation using an option in the constructor (see the `NRelay` class below).\n- Using a `break` statement in the `req` loop will close the subscription automatically, sending a `CLOSE` message to the relay. This works thanks to special treatment of `try...finally` blocks by AsyncIterables.\n- Passing an `AbortSignal` into the `req` method will also close the subscription automatically when the signal aborts, sending a `CLOSE` message.\n\n### `NRelay1` class\n\nThe main `NRelay` implementation for connecting to one relay.\nInstantiate it with a WebSocket URL, and then loop over the messages:\n\n```ts\nconst relay = new NRelay1('wss://relay.mostr.pub');\n\nfor await (const msg of relay.req([{ kinds: [1] }])) {\n  if (msg[0] === 'EVENT') console.log(msg[2]);\n  if (msg[0] === 'EOSE') break; // Sends a `CLOSE` message to the relay.\n}\n```\n\nIf the WebSocket disconnects, it will reconnect automatically thanks to the wonderful [websocket-ts](https://github.com/jjxxs/websocket-ts) library.\nUpon reconnection, it will automatically re-subscribe to all subscriptions.\n\n#### `NRelay1Opts` interface\n\nAll options are optional.\n\n- `auth` - A function like `(challenge: string) =\u003e Promise\u003cNostrEvent\u003e`. If provided, it will be called whenever the relay sends an `AUTH` message, and then it will send the resulting event back to the relay in an `AUTH` message. If not provided, auth is ignored.\n- `backoff` - A [`Backoff`](https://github.com/jjxxs/websocket-ts/blob/v2.1.5/src/backoff/backoff.ts) object for reconnection attempts, or `false` to disable automatic reconnect. Default is `new ExponentialBackoff(1000)`.\n- `verifyEvent` - Custom event verification function. Default is `nostrTools.verifyEvent`.\n\n### `NPool` class\n\nThe `NPool` class is a `NRelay` implementation for connecting to multiple relays.\n\n```ts\nconst pool = new NPool({\n  open: (url) =\u003e new NRelay1(url),\n  reqRelays: async (filters) =\u003e ['wss://relay1.mostr.pub', 'wss://relay2.mostr.pub'],\n  eventRelays: async (event) =\u003e ['wss://relay1.mostr.pub', 'wss://relay2.mostr.pub'],\n});\n\n// Now you can use the pool like a regular relay.\nfor await (const msg of pool.req([{ kinds: [1] }])) {\n  if (msg[0] === 'EVENT') console.log(msg[2]);\n  if (msg[0] === 'EOSE') break;\n}\n```\n\nThis class is designed with the Outbox model in mind.\nInstead of passing relay URLs into each method, you pass functions into the contructor that statically-analyze filters and events to determine which relays to use for requesting and publishing events.\nIf a relay wasn't already connected, it will be opened automatically.\nDefining `open` will also let you use any relay implementation, such as `NRelay1`.\n\nNote that `pool.req` may stream duplicate events, while `pool.query` will correctly process replaceable events and deletions within the event set before returning them.\n\n`pool.req` will only emit an `EOSE` when all relays in its set have emitted an `EOSE`, and likewise for `CLOSED`.\n\n#### `NPoolOpts` interface\n\n- `open` - A function like `(url: string) =\u003e NRelay`. This function should return a new instance of `NRelay` for the given URL.\n\n- `reqRelays` - A function like `(filters: NostrFilter[]) =\u003e Promise\u003cstring[]\u003e`. This function should return an array of relay URLs to use for making a REQ to the given filters. To support the Outbox model, it should analyze the `authors` field of the filters.\n\n- `eventRelays` - A function like `(event: NostrEvent) =\u003e Promise\u003cstring[]\u003e`. This function should return an array of relay URLs to use for publishing an EVENT. To support the Outbox model, it should analyze the `pubkey` field of the event.\n\nPro-tip: the `url` parameter is a unique relay identifier (string), and doesn't technically _have_ to be a URL, as long as you handle it correctly in your `open` function.\n\n## Signers\n\nSigner, like storages, should be usable in an interoperable/composable way.\nThe foundation of this is NIP-07.\n\n### `NostrSigner` interface\n\nThe `NostrSigner` interface is pulled right out of NIP-07.\nThis means any signer implementing it can be used as a drop-in replacement for `window.nostr`.\nSince NIP-07 functions don't accept many options, new Signers are created by abusing constructor props.\n\n```ts\n/** NIP-07 Nostr signer. */\ninterface NostrSigner {\n  /** Returns a public key as hex. */\n  getPublicKey(): Promise\u003cstring\u003e;\n  /** Takes an event template, adds `id`, `pubkey` and `sig` and returns it. */\n  signEvent(event: Omit\u003cNostrEvent, 'id' | 'pubkey' | 'sig'\u003e): Promise\u003cNostrEvent\u003e;\n  /** Returns a record of relay URLs to relay policies. */\n  getRelays?(): Promise\u003cRecord\u003cstring, { read: boolean; write: boolean }\u003e\u003e;\n  /** @deprecated NIP-04 crypto methods. Use `nip44` instead. */\n  nip04?: {\n    /** @deprecated Returns ciphertext and iv as specified in NIP-04. */\n    encrypt(pubkey: string, plaintext: string): Promise\u003cstring\u003e;\n    /** @deprecated Takes ciphertext and iv as specified in NIP-04. */\n    decrypt(pubkey: string, ciphertext: string): Promise\u003cstring\u003e;\n  };\n  /** NIP-44 crypto methods. */\n  nip44?: {\n    /** Returns ciphertext as specified in NIP-44. */\n    encrypt(pubkey: string, plaintext: string): Promise\u003cstring\u003e;\n    /** Takes ciphertext as specified in NIP-44. */\n    decrypt(pubkey: string, ciphertext: string): Promise\u003cstring\u003e;\n  };\n}\n```\n\n### `NSecSigner` class\n\nNIP-07-compatible signer with secret key. It is a drop-in replacement for `window.nostr`.\n\nUsage:\n\n```ts\nconst signer = new NSecSigner(secretKey);\nconst pubkey = await signer.getPublicKey();\nconst event = await signer.signEvent({ kind: 1, content: 'Hello, world!', tags: [], created_at: 0 });\n```\n\n### `NSeedSigner` class\n\nAccepts an HD seed which it uses to derive the secret key according to [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md).\nThis method is useful for supporting multiple accounts for the same user, or for sharing a Nostr account with a Bitcoin wallet.\n\n```ts\nconst signer = new NSeedSigner(seed, 0);\n\nsigner.getPublicKey();\nsigner.signEvent(t);\n```\n\n### `NPhraseSigner` class\n\nSimilar to `NSeedSigner`, but accepts a BIP-39 mnemonic phrase which it converts into a seed before usage.\n\n```ts\nconst signer = new NPhraseSigner('abandon baby cabbage dad ...', {\n  account: 0, // Optional account number. Default is 0.\n  passphrase: 'very special mother', // Optional passphrase. Default is no passphrase.\n});\n\nsigner.getPublicKey();\nsigner.signEvent(t);\n```\n\n### `NCustodial` class\n\nSigner manager for multiple users.\nPass a shared secret into it, then it will generate keys for your users determinstically.\nUseful for custodial auth where you only want to manage one secret for the entire application.\n\n```ts\nconst SECRET_KEY = Deno.env.get('SECRET_KEY'); // generate with `openssl rand -base64 48`\nconst seed = new TextEncoder().encode(SECRET_KEY);\n\nconst signers = new NCustodial(seed);\n\nconst alex = await signers.get('alex');\nconst fiatjaf = await signers.get('fiatjaf');\n\nalex.getPublicKey();\nfiatjaf.signEvent(t);\n```\n\n### `NConnectSigner` class\n\nTODO\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoapbox-pub%2Fnostrify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoapbox-pub%2Fnostrify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoapbox-pub%2Fnostrify/lists"}