A Supabase-style TypeScript SDK for Nostr apps, built on Applesauce.
It translates familiar database calls into signed Nostr events. Applesauce handles signing, relay connections, and the local event store.
flowchart LR
App[Website or app] --> SDK[nostrbase SDK]
SDK --> Signer[Nostr signer]
SDK <-->|Signed events and queries| Relay[Nostr relay]
SDK --> App
import { createClient } from "nostrbase";
const db = createClient({
namespace: "my-app",
relays: ["wss://your-relay.example"],
});
await db.auth.signInWithExtension();The namespace identifies your app's data. Sign-in connects a Nostr signer. The user's public key is their identity.
await db.from("todos").insert({
id: "task-1",
title: "Build my website",
done: false,
});The SDK:
- Converts the record to JSON.
- Creates a kind
30078Nostr event. - Adds tags for the app, table, and record ID.
- Requests the user's signature.
- Sends the event to the relays and collects their responses.
A record's identity is app + table + author + ID.
const { data, error } = await db
.from("todos")
.select()
.eq("done", false);The SDK requests table events from the relays, verifies signatures, and selects the latest version of each record. It then applies field filters and returns normal JavaScript objects.
Author and ID filters run on the relay. Field filters and sorting run on the client. Cursor pages narrow relay requests by time, then resolve the latest record versions before selecting rows.
An update publishes a new signed event at the same record address. The latest version wins.
A delete publishes a NIP-09 deletion request. The SDK hides the targeted record; relays decide how they handle stored copies.
Channels keep a relay subscription open. The SDK compares incoming events with its local state and emits INSERT, UPDATE, or DELETE.
Broadcast and Presence use signed, public ephemeral events. Negentropy (NIP-77) recovers stored records missed during a disconnect. Ordinary queries provide recovery when a relay does not support it.
import { IndexedDBPersistenceAdapter } from "nostrbase";
const db = createClient({
namespace: "my-app",
relays: ["wss://your-relay.example"],
persistence: { adapter: new IndexedDBPersistenceAdapter("my-app-cache") },
sync: { tables: ["todos"], initial: true, reconnect: true },
});
await db.auth.signInWithExtension();
await db.ready();
await db.from("todos").insert({ title: "Work offline", done: false }).queue();
await db.offline.flush(); // Explicitly send saved signed writes when connected.
await db.private.from("notes").insert({ text: "Only for me" });
await db.closeAsync(); // Finish pending cache writes before closing.Set offline: { autoReplay: true } to replay the active account's public, personal, and group queues after startup, enqueue, reconnect, or browser online events. Failed delivery retries with bounded delays. Manual replay remains available. See automatic replay for cancellation, receipts, and device limits.
Private tables encrypt the record body to the user's own key with NIP-44. The signer must support NIP-44. Relays still see the author, app, table, record ID, and timestamps. Public and private tables use separate addresses.
Your app needs no separate application server for these record operations. Relays provide storage and delivery. Files use a separate Blossom HTTP server. Authors edit their own records; application rules run in the client.
const created = await db.groups.create({ name: "Team workspace" });
if (created.error || !created.data) throw created.error ?? new Error("Group unavailable");
const group = created.data;
// Bob first publishes a Marmot KeyPackage from his own client.
const invitation = await group.invite(bobPubkey);
if (invitation.error) throw invitation.error;
const write = await db.from("tasks").inGroup(group.id)
.insert({ title: "Build the website", done: false });
if (write.error) throw write.error;
const { data, error } = await db.from("tasks").inGroup(group.id).select();
if (error) throw error;Marmot manages membership and MLS encryption. Applesauce sends ciphertext through Nostr relays. Members read shared records; each author edits their own records. New members receive signed record snapshots. Durable device state uses a separate self-encrypted adapter.
.inGroup(groupId) explicitly selects the encrypted collection. An unavailable group returns an error; the query cannot fall back to a public table. group.from("tasks") remains an alias. Group creation, lookup, listing, invitations, and joining return { data, error, meta? }. After a partial join or write, inspect returned data and meta.receipts before retrying.
The query builder is Supabase-style. .inGroup() and group management are Nostrbase extensions, not Supabase methods. Supabase usually uses membership tables and Row Level Security. This SDK has no configurable server access policies. Group sync() uses ordinary Nostr queries, not Negentropy. The Marmot integration is experimental and uses an unreleased engine snapshot. See shared private collections for setup, recovery, and limits.
The SDK provides the familiar client API and the following Nostr equivalents. These features do not supply PostgreSQL guarantees or the full Supabase platform.
| Supabase feature | Available? | SDK behavior |
|---|---|---|
| TypeScript client | ✅ | createClient, typed tables, { data, error } results |
| Insert, read, update, delete | ✅ | Records stored as signed Nostr events |
| Upsert | ✅ | Creates or replaces an author's record |
| Filters and field selection | ✅ | Logical OR/NOT, patterns, nested JSON, collection filters, null ordering, typed projections, and local counts |
| Sorting and pagination | ✅, with limits | Local sorting/ranges and newest-first cursor pages; relay results can be incomplete |
| Authentication | Partial | Extension, private key, or Applesauce remote signer |
| Key backup and recovery | ✅, local keys | Password-encrypted NIP-49 ncryptsec; extension/remote key export remains with its signer |
| Live database changes | ✅ | INSERT, UPDATE, DELETE subscriptions |
| Realtime Broadcast | ✅ | Signed public ephemeral channel messages |
| Realtime Presence | ✅ | Public session state, heartbeats, and expiry; approximate |
| Reconnect recovery | ✅ | Public/personal records use Negentropy with query fallback; group sync uses ordinary queries |
| File storage and uploads | ✅, external service | Blossom upload, download, list, and delete with hash checks |
| Image transformations | ✅, local | Browser Canvas or optional Node Sharp adapter; resize, crop, rotate, mirror, and raster conversion |
| Edge Functions / RPC | ❌ | Not implemented |
| PostgreSQL and SQL | ❌ | Uses Nostr events |
| Joins and foreign keys | Partial | Author-scoped references resolved on the client; no enforced foreign keys or SQL joins |
| Transactions | ❌ | Writes can succeed independently |
| Global uniqueness constraints | ❌ | Record identity includes the author |
| Row Level Security | Partial equivalent | Author ownership and encrypted group membership; no configurable server access policies |
| Email/password, OAuth, magic links, MFA | ❌ | Nostr signer auth only |
| Private tables | ✅, personal; experimental shared | NIP-44 personal records and Marmot/MLS group collections |
| Full-text search | Partial | Local table text search; NIP-50 raw event search on supporting relays |
| Vector search | ❌ | Needs a separate index/service |
| Schema validation | ✅, client-side | Zod or custom validators and inferred types; no relay-enforced schema |
| Data migrations | ✅, client-side | Validate transformations, preview, and sign rewrites of your own records |
| Backups | ✅, local | Export/import verified cached events and tombstones; no automatic republishing |
| Dashboard and logs | ✅, local | Read-only cache inspector and bounded SDK diagnostics |
| Offline persistence and write queue | ✅ | IndexedDB cache, exact signed replay, and opt-in automatic retries; memory by default |
Two distinctions matter:
- Auth identifies who signed a record. It does not restrict who can read public records.
- Queries operate over relay results. They do not guarantee a complete global table.
You can build todo apps, profiles, feeds, directories, personal encrypted notes, and apps that attach files. Experimental private collaboration uses Marmot groups. Transactions, server functions, global constraints, and server-enforced business rules need more infrastructure or protocols.
Version 0.2.0 supports modern browsers and Node 22.12+ as an ESM package. It is available locally and has not been published to npm.
See the API and setup guide for installation, typed schemas, auth, queries, channels, and relay write receipts. See the examples, record protocol, and contributor guide for more detail.
Feature guides: key backup and recovery, richer queries, automatic queue replay, image processing, Broadcast and Presence, offline cache and Negentropy, private records and Blossom, and search and developer tools.
Try Fieldwork, a complete example app with a project board, private notebook, files, live updates, and offline writes. It consumes the packed SDK and has real browser tests against independent local services. See its developer experience report.
Verification: test contracts, separate environment projects, and local results.
Run npm ci --prefix site, then npm run docs:dev for the full documentation site at http://127.0.0.1:4321. Build static HTML with npm run docs:build. See the documentation index for setup and authoring.