Client

Build a local-first to-do app with Jazz, step by step.

Create a project

Starting a brand-new app? Run pnpm create jazz instead and follow the Quickstart. It scaffolds a complete Jazz app for you, so you can skip the manual steps below.

The rest of this page walks through adding Jazz by hand, either to a new project you set up yourself or to an existing client app. If you already have an app, skip to Install. Otherwise, create one with your framework's usual tooling:

Terminal
pnpm create vite my-jazz-app --template react-ts
cd my-jazz-app
pnpm install

Install

Terminal
pnpm add jazz-tools@alpha

Get an app ID

Your app ID namespaces your data for storage and sync. Click below to generate one on Jazz Cloud (sync URL: https://v2.sync.jazz.tools/) — you'll also get the secrets you need to deploy your schema and permissions later. Generated apps are unclaimed until you claim them in the dashboard, and unclaimed apps are automatically deleted after 14 days.

Or from the command line (AI agents: use this to provision your own app):

curl -X POST https://v2.dashboard.jazz.tools/api/apps/generate

Jazz Cloud sync URL: https://v2.sync.jazz.tools/

Define your schema

Create schema.ts at the root of your project (or src/lib/schema.ts for SvelteKit). This is the source of truth for your data model.

schema.ts
import { schema as s } from "jazz-tools";
​
const schema = {
projects: s.table(
{
name: s.string(),
},
{ todos: s.reverse("todos", "project") },
),
todos: s.table(
{
title: s.string(),
done: s.boolean(),
description: s.string().optional(),
parentId: s.uuid().optional(),
projectId: s.uuid().optional(),
},
{
parent: s.rel("todos", "parentId"),
children: s.reverse("todos", "parent"),
project: s.rel("projects", "projectId"),
},
),
};
​
type AppSchema = s.Schema<typeof schema>;
export const app: s.App<AppSchema> = s.defineApp(schema);
​
export type Todo = s.RowOf<typeof app.todos>;
export type TodoQueryBuilder = ReturnType<typeof app.todos.limit>;

The most common DSL column builders are s.string(), s.boolean(), s.int(), s.float(), s.timestamp(), s.bytes(), s.uuid(), s.array(s.string()), s.enum("a", "b"), s.json(), and s.json(schema).

For the full TypeScript/SQL mapping table, see Schemas: available column types.

Learn more about schemas, optional validation, and migrations.

Set up your app

Create the basic structure for your app with Jazz and a to-do list.

src/App.tsx
import type { AccountHandle } from "jazz-tools";
import { JazzProvider } from "jazz-tools/react";
import { TodoList } from "./TodoList.js";
​
// Prepare the account outside the context with createAccountManager.
export default function App({ account }: { account: AccountHandle }) {
return (
<JazzProvider
config={{
appId: "<your-app-id>",
account,
}}
>
<h1>Todos</h1>
<TodoList />
</JazzProvider>
);
}

appId identifies your app for storage and sync. Use the UUID you generated above — not a human-readable name — so your local data is already correctly namespaced when you add a server. For all client config options and runtime source overrides, see Client Setup.

Add a to-do

Use db.insert to create a new row.

src/AddTodo.tsx
import { useState } from "react";
import { useDb } from "jazz-tools/react";
import { app } from "../schema.js";
​
export function AddTodo() {
const db = useDb();
const [title, setTitle] = useState("");
​
return (
<form
onSubmit={(e) => {
e.preventDefault();
db.insert(app.todos, { title, done: false });
setTitle("");
}}
>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
);
}

Display and edit a to-do

Display a to-do with toggle and delete controls.

src/TodoItem.tsx
import { useDb, useAll } from "jazz-tools/react";
import { app } from "../schema.js";
​
export function TodoItem({ id }: { id: string }) {
const db = useDb();
const { data: todos = [] } = useAll(app.todos.where({ id }).limit(1));
const [todo] = todos;
​
if (!todo) return null;
​
return (
<li className={todo.done ? "done" : ""}>
<input
type="checkbox"
checked={todo.done}
onChange={() => db.update(app.todos, id, { done: !todo.done })}
/>
<span>{todo.title}</span>
<button onClick={() => db.delete(app.todos, id)}>&times;</button>
</li>
);
}

Full mutation API: Writing Data.

With the default persistent browser driver, Jazz saves writes locally and updates your UI immediately — even while offline.

List to-dos

Subscribe to a query to get real-time updates as data changes, regardless of where the change originates.

src/TodoList.tsx
import { useAll } from "jazz-tools/react";
import { app } from "../schema.js";
import { TodoItem } from "./TodoItem.js";
import { AddTodo } from "./AddTodo.js";
​
export function TodoList() {
const { data: todos = [] } = useAll(app.todos);
​
return (
<>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} id={todo.id} />
))}
</ul>
<AddTodo />
</>
);
}

Framework hooks return undefined while loading, then an array of matching rows. For filtering, sorting, and pagination, see Queries.

Enable sync

So far, your to-do list only works locally. To sync across devices, deploy your schema and permissions to the server.

Or from the command line (AI agents: use this to provision your own app):

curl -X POST https://v2.dashboard.jazz.tools/api/apps/generate

Jazz Cloud sync URL: https://v2.sync.jazz.tools/

Add permissions

Without permissions, permission-scoped server reads return no rows and writes are rejected. For this guide, allow everything:

permissions.ts
import { schema as s } from "jazz-tools";
import { app } from "./schema.js";
​
export default s.definePermissions(app, ({ policy }) => {
policy.projects.allowRead.always();
policy.projects.allowInsert.always();
​
policy.todos.allowRead.always();
policy.todos.allowInsert.always();
policy.todos.allowUpdate.always();
policy.todos.allowDelete.always();
});

Then run deploy from the directory containing both schema.ts and permissions.ts, using the app ID and admin secret from above:

pnpm dlx jazz-tools@alpha deploy \
<your-app-id> \
--server-url https://v2.sync.jazz.tools/ \
--admin-secret <your-admin-secret>

Connect the client

Update your client config to add serverUrl — Jazz Cloud is at https://v2.sync.jazz.tools/. If you generated an app ID above, these values are already filled in:

{
appId: "<your-app-id>",
serverUrl: "https://v2.sync.jazz.tools/",
}

When you update schema.ts, run deploy again. Compatible schema changes can use an inferred migration; changes that transform existing rows need a reviewed migration path before deployment.

Permissions can be updated without a schema migration by re-running pnpm dlx jazz-tools@alpha deploy <appId> with both files present.

For self-hosted deployments, see Server Setup.

Next steps

Example apps