🏡/repos/garden-co/

jazz

🎷

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.

TypeScriptReactLocal-First
💡

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

jazz-tools
High-level API with Zod schemas, React hooks, auth providers
cojson
Core collaborative JSON engine - LocalNode, SyncManager, Permissions
Storage Adapters
IndexedDB, SQLite, Durable Objects
Transport Adapters
WebSocket, custom channels
📦

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 undefined
📥

Data 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)  // Clear

Delete

// 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.title
⚛️

React 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

admin

Full control + manage members

manager

Manage non-admin members

writer

Read and write

reader

Read only

writeOnly

Write, 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 write

Invite 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

LoadMessage

Request data for a CoValue

action: 'load'id: coValueIdsessionID: txCount
KnownStateMessage

Acknowledge received state

action: 'known'id: coValueIdsessionID: txCount
NewContentMessage

Send new transactions

action: 'content'header: CoValueHeadernew: { transactions }
DoneMessage

Sync handshake complete

action: 'done'id: coValueId

Transaction 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.