Jazz: a distributed database that lives in your frontend

November 22, 2025

|repo-review

by Florian Narr

Jazz: a distributed database that lives in your frontend

Jazz describes itself as "a new kind of database that's distributed across your frontend, containers, serverless functions and its own storage cloud." That's doing a lot of work in one sentence, but it's not wrong.

The short version: you define typed collaborative data structures, wrap your app in <JazzReactProvider>, and get real-time sync, offline support, multiplayer, E2E encryption, and invite-based sharing — without writing a single API endpoint.

Why I starred it

Most "real-time sync" solutions make you pick a lane: either you own a WebSocket server and write sync logic yourself, or you pay for Firebase and give up control. Jazz takes a different approach borrowed from the local-first movement: data lives on the client first, and a sync server is just one peer among many.

What caught my eye was the CRDT layer underneath — cojson, the core package — combined with a schema system that feels more like Zod than a traditional ORM. It's opinionated in the right places.

How it works

The repo is a pnpm monorepo. The meat is in packages/cojson (the low-level sync engine) and packages/jazz-tools (the typed API you actually use).

CoValues are the core abstraction. Everything is a CoValue — maps, lists, streams, accounts, groups. Each one has a globally unique CoID, a CoValueHeader that declares its type and permission ruleset, and a log of signed transactions. The header structure from packages/cojson/src/coValueCore/verifiedState.ts:

export type CoValueHeader = {
  type: AnyRawCoValue["type"];
  ruleset: RulesetDef;
  meta: JsonObject | null;
} & CoValueUniqueness;

export type Transaction = PrivateTransaction | TrustingTransaction;

export type PrivateTransaction = {
  privacy: "private";
  madeAt: number;
  keyUsed: KeyID;
  encryptedChanges: Encrypted<JsonValue[], { in: RawCoID; tx: TransactionID }>;
};

Every mutation is a transaction, either encrypted (private) or in the clear (trusting). The sync protocol in packages/cojson/src/sync.ts exchanges five message types: load, known, content, done, and reconcile. Peers advertise what sessions they know about via KnownStateMessage, and then stream deltas as NewContentMessage. The delta is per-session: each session has an index counter, and you just send everything after after: N.

Permissions are enforced client-side through the determineValidTransactions() function in packages/cojson/src/permissions.ts. Groups are first-class CoValues that store member roles (reader, writer, admin, manager, writeOnly). A CoValue's header points to its owning group; transactions from unauthorized signers are silently dropped. The roles even support invite tokens — readerInvite, writerInvite, etc. — which map to URL-shareable secrets.

The high-level API in jazz-tools wraps all of this behind a Zod-flavored schema builder:

export const Task = co
  .map({
    done: z.boolean(),
    text: co.plainText(),
    version: z.literal(1),
  })
  .withMigration((task) => {
    if (!task.version) {
      task.$jazz.set("text", task.text);
      task.$jazz.set("version", 1);
    }
  });

export const TodoProject = co.map({
  title: z.string(),
  tasks: co.list(Task),
});

Migrations run on load, which is how you handle schema evolution across clients that may be on different versions simultaneously. That's a genuinely hard problem and Jazz has a real answer for it.

The deepLoading.ts file in jazz-tools manages the $isLoaded / MaybeLoaded<T> type pattern. CoValues load lazily, so refs start as NotLoaded until the sync layer delivers the data. You query with a .resolved() shape to declare what you need eagerly:

export const TodoProjectWithTasks = TodoProject.resolved({
  tasks: {
    $each: TaskWithText.resolveQuery,
  },
});

This is how they avoid waterfalls without requiring you to manually orchestrate fetches.

The LocalNode in packages/cojson/src/localNode.ts is the peer coordinator. It holds a Map<RawCoID, CoValueCore>, manages a SyncManager, and optionally runs a GarbageCollector. You attach peers (WebSocket transport, IndexedDB storage, SQLite) and it handles reconciliation automatically.

Using it

npx create-jazz-app@latest my-app --framework react
cd my-app && npm install && npm run dev

In your app:

<JazzReactProvider
  sync={{ peer: "wss://cloud.jazz.tools/?ke=YOUR_KEY" }}
  AccountSchema={MyAccount}
>
  <App />
</JazzReactProvider>

Then in any component:

const me = useAccount(TodoAccountWithProjects);
const { root } = me; // reactive, auto-synced

root.projects.push(
  TodoProject.create({ title: "New project", tasks: [] }, { owner: me })
);

No REST endpoints. No useEffect + fetch. No manual cache invalidation.

Jazz Cloud (cloud.jazz.tools) is the zero-config sync server. It's free for small usage and you can self-host via the jazz-run package or a Cloudflare Durable Objects setup (there's an example in examples/durable-object).

Rough edges

The docs at jazz.tools are actively being written and still have gaps. The API surface is large — CoMap, CoList, CoFeed, CoPlainText, CoRichText, CoVector, accounts, groups, inbox, webhooks — and finding which primitive to reach for isn't always obvious.

The migration system is clever but puts real responsibility on you: if two clients run different code simultaneously, you're in territory where things can get weird. The framework doesn't guard against bad migrations.

Framework support is good for React and decent for Svelte, but Vue is community-maintained and still experimental. React Native works but has a separate package (jazz-tools/react-native) with some limitations around auth.

There's no query language. You navigate the object graph directly, which is ergonomic for tree-shaped data but awkward if you need filtered lists or aggregations across many CoValues.

Bottom line

If you're building a collaborative, multiplayer, or offline-capable app in React and want to skip the entire backend sync layer, Jazz is the most complete local-first framework I've seen in the TypeScript ecosystem. The CRDT foundation is solid, the permission model is actually expressive, and the DX is genuinely good once you understand the CoValue mental model.

garden-co/jazz on GitHub
garden-co/jazz