Jazz
Distributed sync-first database for local-first apps
Reactive local JSON state that syncs instantly. Built-in collaboration, encryption, offline support, and permissions. Works across browsers, servers, and serverless.
Core Philosophy
Local-First
Data feels like local state. Changes apply instantly, sync in background. Full offline support.
Collaboration Built-In
Real-time multiplayer, edit history, and fine-grained permissions from day one.
Privacy by Default
End-to-end encryption, secure key distribution. Only authorized users can decrypt.
Architecture
Layered design for flexibility
CoValue Types
Collaborative data structures
CoMap
Collaborative object/record. Like a typed JSON object that syncs.
class Person extends CoMap {
name = coField.string
age = coField.number
avatar = coField.ref(Image)
}CoList
Collaborative array. Ordered collection with CRDT semantics.
class TodoList extends CoList.Of(coField.ref(Task)) {}
const list = TodoList.create([task1, task2])
list.$jazz.push(newTask)
list.$jazz.remove(t => t.done)CoPlainText
CRDT text field for collaborative editing without conflicts.
class Note extends CoMap {
title = coField.string
body = co.plainText() // Collaborative text
}CoRichText
Rich text with formatting. Integrates with ProseMirror.
class Document extends CoMap {
content = co.richText()
}
// Use with prose editor
const editor = useJazzProseEditor(doc.content)Group
Access control container. Members have roles (admin, writer, reader).
const group = Group.create()
group.addMember(user, "writer")
group.makePublic("reader")
const project = Project.create(data, { owner: group })Account
User identity with profile and app-specific root data.
class MyAccount extends Account {
profile = co.profile() // name, avatar
root = co.map({
projects: co.list(Project)
})
}FileStream
Efficient streaming file uploads/downloads.
class Post extends CoMap {
image = co.file()
}
post.image = await FileStream.create(
() => fetch(url).then(r => r.blob()),
{ owner: group }
)CoFeed / CoStream
Append-only event logs for activity feeds, chat, etc.
class ChatMessages extends CoStream.Of({
text: coField.string,
timestamp: coField.encoded(Encoders.Date),
}) {}
chat.$jazz.push({ text: "Hello", timestamp: new Date() })Schema Definition
Type-safe with Zod integration
Defining Schemas
import { co, z, coField } from "jazz-tools"
// Task schema
export const Task = co.map({
done: z.boolean(),
text: co.plainText(),
priority: z.literal("low", "medium", "high"),
dueDate: coField.optional.Date,
assignee: coField.optional.ref(Account),
})
// Project schema with nested tasks
export const Project = co.map({
title: z.string(),
tasks: co.list(Task),
owner: coField.ref(Account),
})
// Account schema with migrations
export const MyAccount = co
.account({
profile: co.profile(),
root: co.map({
projects: co.list(Project),
}),
})
.withMigration(async (account) => {
// Initialize on first login
if (!account.$jazz.has("root")) {
account.$jazz.set("root", { projects: [] })
}
})
// Type inference
type Project = co.loaded<typeof Project>Resolved Queries (Deep Loading)
// Define what to load upfront
export const ProjectWithTasks = Project.resolved({
tasks: {
$each: { // Load each task in the list
text: true, // Load text field
assignee: { // Load assignee reference
profile: true
}
},
},
owner: {
profile: true
}
})
// Type knows everything is loaded
type ProjectWithTasks = co.loaded<typeof ProjectWithTasks>
// project.tasks[0].assignee.profile.name is string, not undefinedData Loading
Reactive subscriptions and queries
Primary Pattern: useCoState
Subscribe to CoValues reactively. Components re-render when data changes locally or from sync.
useCoState - Reactive Hook
import { useCoState } from "jazz-tools/react"
function ProjectView({ projectId }: { projectId: string }) {
// Subscribe and load with resolve query
const project = useCoState(ProjectWithTasks, projectId)
// Loading state
if (project.$isLoaded === false) {
return <div>Loading...</div>
}
// Error state
if (project.$isLoaded === "error") {
return <div>Error loading project</div>
}
// Data is loaded - TypeScript knows all fields exist
return (
<div>
<h1>{project.title}</h1>
<p>Owner: {project.owner.profile.name}</p>
{project.tasks.map((task) => (
<TaskRow key={task.$jazz.id} task={task} />
))}
</div>
)
}useAccount - Current User
import { useAccount } from "jazz-tools/react"
function Dashboard() {
const me = useAccount(MyAccount.resolved({
root: {
projects: { $each: {} }
}
}))
if (me.$isLoaded === false) return <Loading />
return (
<div>
<h1>Welcome, {me.profile?.name}</h1>
<h2>Your Projects</h2>
{me.root.projects.map((project) => (
<ProjectCard key={project.$jazz.id} project={project} />
))}
</div>
)
}subscribeToCoValue
import { subscribeToCoValue } from "jazz-tools"
const unsubscribe = subscribeToCoValue(
ProjectWithTasks,
projectId,
{ resolve: ProjectWithTasks.resolveQuery },
(project) => {
console.log("Updated:", project.title)
}
)
// Cleanup
unsubscribe()loadCoValue (One-time)
import { loadCoValue } from "jazz-tools"
const project = await loadCoValue(
ProjectWithTasks,
projectId,
localNode,
{ resolve: ProjectWithTasks.resolveQuery }
)
// Use in server functions, scripts, etc.CRUD Operations
Create, Read, Update, Delete
Create
import { Group } from "jazz-tools"
// Create with access control group
const group = Group.create()
group.addMember(collaborator, "writer")
const project = Project.create(
{
title: "New Project",
tasks: [],
owner: me,
},
{ owner: group } // Access controlled by group
)
// ID is auto-generated
console.log(project.$jazz.id) // "co_z..."
// Add to account
me.root.projects.$jazz.push(project)Update
// Direct assignment (primitives)
project.title = "Updated Title" // Syncs instantly
// Via $jazz.set (all fields)
project.$jazz.set("title", "New Title")
// List operations
project.tasks.$jazz.push(newTask)
project.tasks.$jazz.set(0, updatedTask)
project.tasks.$jazz.remove((t) => t.$jazz.id === taskId)
// Text operations (CoPlainText)
task.text.$jazz.insert(0, "Prepended: ")
task.text.$jazz.delete(0, 5)
task.text.$jazz.splice(startIndex, deleteCount, ...insertItems)
// Optional fields
project.$jazz.set("dueDate", new Date())
project.$jazz.set("dueDate", null) // ClearDelete
// Remove from list
project.tasks.$jazz.remove((task) => task.$jazz.id === taskId)
// Remove by index
project.tasks.$jazz.splice(index, 1)
// Clear all
project.tasks.$jazz.splice(0, project.tasks.length)
// Remove project from account
me.root.projects.$jazz.remove((p) => p.$jazz.id === projectId)Edit History
// Get all edits for a field
const edits = project.$jazz.edits.title
// Array of: { value, by: Account | null, madeAt: Date }
// Latest edit
const lastEdit = edits[edits.length - 1]
console.log(`Changed by ${lastEdit.by?.profile?.name} at ${lastEdit.madeAt}`)
// Full history with refs
const allEdits = project.$jazz.allEdits.titleReact Integration
Provider setup and hooks
Provider Setup
import { JazzReactProvider, PassphraseAuthBasicUI } from "jazz-tools/react"
import { wordlist } from "@scure/bip39/wordlists/english"
function App() {
return (
<JazzReactProvider
sync={{
peer: "wss://cloud.jazz.tools/?key=YOUR_API_KEY",
}}
AccountSchema={MyAccount}
>
<PassphraseAuthBasicUI appName="My App" wordlist={wordlist}>
<YourApp />
</PassphraseAuthBasicUI>
</JazzReactProvider>
)
}Available Hooks
useCoState(Schema, id)Subscribe to and load a CoValue by ID
useAccount(Schema)Access current authenticated user
useLogOut()Returns logout function
useIsAuthenticated()Check if user is logged in
useAcceptInvite(options)Handle invite links automatically
useJazzContext()Access raw LocalNode, SyncManager
Permissions & Groups
Fine-grained access control
Role Hierarchy
adminFull control + manage members
managerManage non-admin members
writerRead and write
readerRead only
writeOnlyWrite, can't read others
const group = Group.create()
// Add members
group.addMember(alice, "admin")
group.addMember(bob, "writer")
group.addMember(charlie, "reader")
// Check permissions
group.myRole() // "admin" | "writer" | ...
group.canWrite(bob) // true
// Change role
group.$jazz.set(bob.id, "admin")
// Revoke access
group.$jazz.set(charlie.id, "revoked")
// Public access
group.makePublic("reader") // Anyone can read
group.makePublic("writer") // Anyone can writeInvite System
import { createInviteLink, useAcceptInvite } from "jazz-tools"
// Create invite link
const inviteSecret = group.createInvite("writer")
const link = createInviteLink({
inviteSecret,
valueId: project.$jazz.id,
})
// => "https://myapp.com/#/accept/INVITE_SECRET/co_..."
// Accept invite in app
useAcceptInvite({
invitedObjectSchema: Project,
forValueHint: "project",
onAccept: (projectId) => {
router.navigate(`/project/${projectId}`)
},
})Authentication
Multiple auth providers
PassphraseAuth
12-word mnemonic for account recovery. No server-side storage needed.
import { PassphraseAuth } from "jazz-tools"
const auth = new PassphraseAuth(...)
// Register - returns passphrase
const passphrase = await auth.registerNewAccount("John")
// "word1 word2 ... word12"
// Login with passphrase
await auth.logIn("word1 word2 ... word12")DemoAuth
Temporary accounts for testing. No persistence.
import { DemoAuth } from "jazz-tools"
// Auto-creates temporary account
const auth = new DemoAuth()Clerk Integration
import { BrowserClerkAuth } from "jazz-tools"
const auth = new BrowserClerkAuth(clerkClient)BetterAuth Integration
import { JazzBetterAuthServerAdapter } from "jazz-tools/better-auth"
const auth = new JazzBetterAuthServerAdapter()Sync Protocol
How real-time sync works
Message Types
Request data for a CoValue
action: 'load'id: coValueIdsessionID: txCountAcknowledge received state
action: 'known'id: coValueIdsessionID: txCountSend new transactions
action: 'content'header: CoValueHeadernew: { transactions }Sync handshake complete
action: 'done'id: coValueIdTransaction Structure
// Private (encrypted by default)
type PrivateTransaction = {
privacy: "private"
madeAt: number
keyUsed: KeyID
encryptedChanges: Encrypted<JsonValue[]>
}
// Trusting (visible to all group members)
type TrustingTransaction = {
privacy: "trusting"
madeAt: number
changes: JsonValue[]
}Complete Example
Todo app with collaboration
1. Schema (schema.ts)
import { co, z, coField, Account } from "jazz-tools"
export const Task = co.map({
done: z.boolean(),
text: co.plainText(),
})
export const Project = co.map({
title: z.string(),
tasks: co.list(Task),
})
export const TodoAccount = co
.account({
profile: co.profile(),
root: co.map({
projects: co.list(Project),
}),
})
.withMigration(async (account) => {
if (!account.$jazz.has("root")) {
account.$jazz.set("root", { projects: [] })
}
})
// Resolved queries
export const ProjectWithTasks = Project.resolved({
tasks: { $each: { text: true } }
})2. App Setup (main.tsx)
import { JazzReactProvider, PassphraseAuthBasicUI } from "jazz-tools/react"
import { TodoAccount } from "./schema"
function App() {
return (
<JazzReactProvider
sync={{ peer: "wss://cloud.jazz.tools/?key=API_KEY" }}
AccountSchema={TodoAccount}
>
<PassphraseAuthBasicUI appName="Todo" wordlist={wordlist}>
<Router />
</PassphraseAuthBasicUI>
</JazzReactProvider>
)
}3. Project List (ProjectList.tsx)
import { useAccount } from "jazz-tools/react"
import { Group } from "jazz-tools"
import { TodoAccount, Project } from "./schema"
function ProjectList() {
const me = useAccount(TodoAccount.resolved({
root: { projects: { $each: {} } }
}))
if (me.$isLoaded === false) return <Loading />
const createProject = () => {
const group = Group.create()
const project = Project.create(
{ title: "New Project", tasks: [] },
{ owner: group }
)
me.root.projects.$jazz.push(project)
}
return (
<div>
<button onClick={createProject}>New Project</button>
{me.root.projects.map((project) => (
<Link key={project.$jazz.id} to={`/project/${project.$jazz.id}`}>
{project.title}
</Link>
))}
</div>
)
}4. Project View (ProjectView.tsx)
import { useCoState } from "jazz-tools/react"
import { ProjectWithTasks, Task } from "./schema"
function ProjectView({ projectId }: { projectId: string }) {
const project = useCoState(ProjectWithTasks, projectId)
if (project.$isLoaded === false) return <Loading />
const addTask = (text: string) => {
const task = Task.create(
{ done: false, text },
{ owner: project.$jazz.owner }
)
project.tasks.$jazz.push(task)
}
return (
<div>
<input
value={project.title}
onChange={(e) => project.title = e.target.value}
/>
{project.tasks.map((task) => (
<div key={task.$jazz.id}>
<input
type="checkbox"
checked={task.done}
onChange={(e) => task.$jazz.set("done", e.target.checked)}
/>
<span>{task.text}</span>
<button onClick={() =>
project.tasks.$jazz.remove(t => t.$jazz.id === task.$jazz.id)
}>
Delete
</button>
</div>
))}
<NewTaskForm onSubmit={addTask} />
</div>
)
}5. Share & Collaborate
import { createInviteLink, useAcceptInvite } from "jazz-tools"
// Share button
function ShareButton({ project }) {
const share = () => {
const invite = project.$jazz.owner.createInvite("writer")
const link = createInviteLink({
inviteSecret: invite,
valueId: project.$jazz.id,
})
navigator.clipboard.writeText(link)
}
return <button onClick={share}>Copy Share Link</button>
}
// Accept invites (in app root)
function App() {
useAcceptInvite({
invitedObjectSchema: Project,
onAccept: (id) => router.navigate(`/project/${id}`),
})
return <Router />
}Storage & Deployment
Browser
import { IndexedDBStorageAdapter }
from "cojson-storage-indexeddb"
const storage = await IndexedDBStorageAdapter
.create("myapp")Server (SQLite)
import { SQLiteStorageAdapter }
from "cojson-storage-sqlite"
const storage = new SQLiteStorageAdapter(
"./data.db"
)Cloudflare DO
import { DurableObjectStorageAdapter }
from "cojson-storage-do-sqlite"
const storage = new DurableObjectStorageAdapter(
env.STORAGE
)Advanced Features
Schema Migrations
Handle evolving schemas with .withMigration()
const Task = co.map({
text: co.plainText(),
version: z.literal(2),
}).withMigration((task) => {
if (task.version < 2) {
task.$jazz.set("version", 2)
}
})Vector Search
Store embeddings for similarity search
class Document extends CoMap {
content = coField.string
embedding = co.vector(1536)
}
doc.embedding = [0.1, 0.2, ...]Branching / Time Travel
Create variant branches at specific times
const branch = await loadCoValue(
Project, projectId, node, {
unstable_branch: {
owner: newGroup,
at: pastTimestamp,
},
}
)SSR Support
Server-side rendering with Jazz
import { getServerJazzContext } from "jazz-tools/react/ssr"
export async function getServerSideProps() {
const jazz = await getServerJazzContext()
const data = await jazz.loadCoValue(...)
return { props: { data } }
}Key Takeaways
Local-first by design
Data lives locally, syncs in background. Instant UI updates, full offline support, no loading spinners for local data.
Type-safe end-to-end
Zod schemas provide compile-time safety. Resolved queries ensure deep data is loaded. No runtime surprises.
Permissions are data
Groups are CoValues themselves. Access control syncs like any other data. Encryption keys distributed automatically.
Transactions are immutable
All changes are signed transactions. Full audit trail. CRDT-like conflict resolution. History is always available.