Note: This is the archived v1 of Jazz. Garden Computing has since released Jazz v2. The README flags classic-jazz as legacy, but the architecture is worth understanding — v2 builds directly on these ideas.
Classic Jazz is a local-first sync layer where data lives on the client first and syncs across peers — browsers, containers, serverless functions, and Jazz Cloud — using CRDTs with end-to-end encryption and a built-in role-based permission system.
Why I starred it
The local-first space is crowded with partial solutions: libraries that handle conflict resolution but skip auth, or sync engines that require a central coordinator. Jazz tackles all three — sync, permissions, and encryption — as a single coherent primitive.
The other draw is the layering. There's cojson (the raw CRDT engine), jazz-tools (the typed API with Zod schemas), and then framework adapters for React, Svelte, React Native, and more. You can use the low-level API or sit entirely in the typed layer, never touching the internals.
How it works
CoValues: the unit of everything
Every piece of data is a CoValue — a collaborative object identified by a content-addressed ID derived from its header. The ID is deterministic: idforHeader() in packages/cojson/src/coValueCore/coValueCore.ts hashes the header with BLAKE3 and formats it as co_z<hash>. This means two nodes creating the same logical value independently produce the same ID.
The core value types in packages/cojson/src/coValues/ are:
RawCoMap— LWW map, each key tracks all ops with theirtxIDandmadeAtRawCoList— list CRDT using insertion/deletion ops withpre/apprelative positioningRawCoStream— per-session append-only feedRawBinaryCoStream— chunked binary data (files, images)RawCoPlainText— text CRDT optimized for collaborative editing
RawCoMap in packages/cojson/src/coValues/coMap.ts tracks the full op history per key, but surfaces only latest:
type MapOp<K extends string, V extends JsonValue | undefined> = {
txID: TransactionID;
madeAt: number;
changeIdx: number;
change: MapOpPayload<K, V>;
trusting?: boolean;
};
// The ops history and just the latest resolved value are both kept:
ops: { [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[] } = {};
latest: { [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]> } = {};
Permissions baked into writes
Groups in packages/cojson/src/coValues/group.ts are themselves CoValues. Membership is a map of accountID → role. When you call determineValidTransactions() in packages/cojson/src/permissions.ts, the engine replays the group's transaction log to decide which writes are valid before applying them. Roles include reader, writer, manager, admin, and writeOnly — the last lets you write data that only you can read back (useful for telemetry or personal data inside a shared object).
Encryption is layered on top through sealed read keys. Groups store an encrypted readKey per member (keyUsed field on PrivateTransaction). When a member is revoked, admins rotate the key — old ciphertext stays on disk but new writes become inaccessible. This is all in packages/cojson/src/coValueCore/verifiedState.ts:
export type PrivateTransaction = {
privacy: "private";
madeAt: number;
keyUsed: KeyID;
encryptedChanges: Encrypted<JsonValue[], { in: RawCoID; tx: TransactionID }>;
meta?: Encrypted<JsonObject, { in: RawCoID; tx: TransactionID }>;
};
The crypto layer
The crypto provider in packages/cojson/src/crypto/ has three implementations: WASM (Ed25519 + XSalsa20 + X25519 + BLAKE3, primary browser/Node path), NAPI (native Node.js via napi-rs), and React Native (via the cojson-core-rn Rust crate). The WASM implementation in WasmCrypto.ts uses cojson-core-wasm, a Rust crate compiled to WASM. Three different crypto paths for the same interface is not glamorous engineering, but it is pragmatic given the platform fragmentation.
Sync and clock drift
The LocalNode in packages/cojson/src/localNode.ts is the runtime hub. It holds a Map<RawCoID, CoValueCore> of loaded values and a SyncManager. Each peer connection is a bidirectional channel that exchanges KnownStateMessage and NewContentMessage frames.
There's a detail worth flagging: a recent commit added experimental clock-drift correction (ClockOffset.ts). Writes carry madeAt timestamps, and if a client's wall clock is skewed, causal ordering breaks. The fix uses server ping samples to compute a rolling median offset:
addSample(sample: ClockOffsetSample): void {
const impliedOffset = sample.serverTime - sample.localReceiveTime;
// Outlier gate: reject if too far from the current estimate
if (
this.samples.length > 0 &&
Math.abs(impliedOffset - this.cachedOffset) > this.outlierThresholdMs
) {
return;
}
this.samples.push(impliedOffset);
if (this.samples.length > this.windowSize) {
this.samples.shift();
}
this.cachedOffset = median(this.samples);
}
A 32-sample rolling median, outlier-gated at ±2 seconds, with a hard cap of ±24 hours. The first-sample bootstrapping issue is documented inline — worth watching.
The typed API
jazz-tools wraps the raw layer with a Zod-validated schema system. You define schemas using co.map() and get full TypeScript inference:
import { co, z } from "jazz-tools";
const Pet = co.map({ name: z.string() });
const Person = co.map({
name: z.string(),
age: z.number(),
pet: Pet,
});
Nested CoValue references are lazily loaded — a Person instance knows about its pet reference but won't load the actual object until you subscribe. The DeeplyLoaded<T> and RefsToResolve<T> types in packages/jazz-tools/src/tools/exports.ts encode loading depth at the type level, so TypeScript knows which refs are resolved and which are still ID<Pet>.
Using it
npx create-jazz-app@latest my-app
cd my-app && npm install && npm run dev
In a React app:
import { JazzReactProvider } from "jazz-tools/react";
import { createWebSocketPeer } from "cojson-transport-ws";
function App() {
return (
<JazzReactProvider
sync={{ peer: "wss://cloud.jazz.tools/?ke=your-key" }}
>
<MyApp />
</JazzReactProvider>
);
}
From there, useCoState() gives you reactive access to any CoValue by ID — updates from other clients arrive automatically.
Rough edges
The project is archived — Garden Computing recommends v2 for new work. The README says so plainly at the top.
Beyond that: the multi-platform crypto situation (WASM + NAPI + React Native) adds real maintenance surface. The clock-drift fix is explicitly experimental and the comments acknowledge the first-sample bias problem without a fix yet. Docs exist but are scattered between classic.jazz.tools and in-code JSDoc; finding the right level of abstraction to start from takes some digging.
Test coverage is present but uneven — the CRDT types have reasonable unit tests, but integration tests across multiple LocalNode instances are sparse.
Bottom line
If you're building a collaborative app and want sync, auth, and permissions to be someone else's problem, this architecture is the right shape — study it even if you ship v2. Classic Jazz is a complete reference implementation of local-first CRDT design with production-grade crypto and a clean layering story.
