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:
Terminalpnpm create vite my-jazz-app --template react-tscd my-jazz-apppnpm install
Terminalpnpm create vite my-jazz-app --template vue-tscd my-jazz-apppnpm install
Terminalpnpm create vite my-jazz-app --template svelte-tscd my-jazz-apppnpm install
Terminalpnpm create vite my-jazz-app --template solid-tscd my-jazz-apppnpm install
Terminalmkdir my-jazz-app && cd my-jazz-apppnpm initpnpm add vite typescript
Install
Terminalpnpm add jazz-tools@alpha
Terminalpnpm add jazz-tools@alpha
Terminalpnpm add jazz-tools@alpha
Terminalpnpm add jazz-tools@alpha
Terminalpnpm 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.tsimport { 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.tsximport 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 (<JazzProviderconfig={{appId: "<your-app-id>",account,}}><h1>Todos</h1><TodoList /></JazzProvider>);}
src/App.vue<script setup lang="ts">import type { DbConfig } from "jazz-tools";import { JazzProvider } from "jazz-tools/vue";import TodoList from "./TodoList.vue";defineProps<{ config: DbConfig }>();</script><template><JazzProvider :config="config"><h1>Todos</h1><TodoList /><template #fallback><p>Loading...</p></template></JazzProvider></template>
src/App.svelte<script lang="ts">import type { DbConfig } from "jazz-tools";import { JazzSvelteProvider } from 'jazz-tools/svelte';import TodoList from './TodoList.svelte';let { config }: { config: DbConfig } = $props();</script><JazzSvelteProvider {config}>{#snippet children()}<h1>Todos</h1><TodoList />{/snippet}{#snippet fallback()}<p>Loading...</p>{/snippet}</JazzSvelteProvider>
src/App.tsximport { JazzProvider, type JazzAppConfig } from "jazz-tools/solid";import { TodoList } from "./TodoList.js";export function App(props: { config: JazzAppConfig }) {return (<JazzProvider {...props.config}><h1>Todos</h1><TodoList /></JazzProvider>);}
index.html<!doctype html><html><body><ul id="todos"></ul><script type="module" src="./src/main.ts"></script></body></html>
Create src/main.ts and initialise the database:
src/main.tsimport { createAccountManager, createDb } from "jazz-tools";import { app } from "../schema.js";import { renderTodoItem } from "./TodoItem.js";const appId = "<your-app-id>";const config = { appId, serverUrl: "https://core.example" };const accounts = await createAccountManager(config);const account = accounts.getLoggedIn() ?? accounts.createLocalFirst();const db = await createDb({ ...config, account });// use db.shutdown() to clean up when finished
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.tsximport { 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 (<formonSubmit={(e) => {e.preventDefault();db.insert(app.todos, { title, done: false });setTitle("");}}><inputtype="text"value={title}onChange={(e) => setTitle(e.target.value)}placeholder="What needs to be done?"/><button type="submit">Add</button></form>);}
src/AddTodo.vue<script setup lang="ts">import { ref } from "vue";import { useDb } from "jazz-tools/vue";import { app } from "../schema.js";const db = useDb();const title = ref("");function addTodo() {db.insert(app.todos, { title: title.value, done: false });title.value = "";}</script><template><form @submit.prevent="addTodo"><input v-model="title" type="text" placeholder="What needs to be done?" /><button type="submit">Add</button></form></template>
src/AddTodo.svelte<script lang="ts">import { getDb } from 'jazz-tools/svelte';import { app } from '../schema.js';const db = getDb();let title = $state('');</script><form onsubmit={(e) => {e.preventDefault();db.insert(app.todos, { title, done: false });title = '';}}><input type="text" bind:value={title} placeholder="What needs to be done?" /><button type="submit">Add</button></form>
src/AddTodo.tsximport { createSignal } from "solid-js";import { useDb } from "jazz-tools/solid";import { app } from "../schema.js";export function QuickstartAdd() {const db = useDb();const [title, setTitle] = createSignal("");function addTodo() {db().insert(app.todos, { title: title(), done: false });setTitle("");}return (<formonSubmit={(e) => {e.preventDefault();addTodo();}}><inputvalue={title()}onInput={(e) => setTitle(e.currentTarget.value)}type="text"placeholder="What needs to be done?"/><button type="submit">Add</button></form>);}
src/main.tsconst form = document.createElement("form");const input = Object.assign(document.createElement("input"), {placeholder: "What needs to be done?",});form.append(input, Object.assign(document.createElement("button"), { textContent: "Add" }));form.onsubmit = (e) => {e.preventDefault();db.insert(app.todos, { title: input.value, done: false });input.value = "";};document.body.append(form);
Display and edit a to-do
Display a to-do with toggle and delete controls.
src/TodoItem.tsximport { 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" : ""}><inputtype="checkbox"checked={todo.done}onChange={() => db.update(app.todos, id, { done: !todo.done })}/><span>{todo.title}</span><button onClick={() => db.delete(app.todos, id)}>×</button></li>);}
src/TodoItem.vue<script setup lang="ts">import { computed } from "vue";import { useDb, useAll } from "jazz-tools/vue";import { app } from "../schema.js";const props = defineProps<{ id: string }>();const db = useDb();const { data: todos } = useAll(() => app.todos.where({ id: props.id }).limit(1));const todo = computed(() => todos.value?.[0]);</script><template><li v-if="todo" :class="{ done: todo.done }"><inputtype="checkbox":checked="todo.done"@change="db.update(app.todos, props.id, { done: !todo.done })"/><span>{{ todo.title }}</span><button @click="db.delete(app.todos, props.id)">×</button></li></template>
src/TodoItem.svelte<script lang="ts">import { getDb, QuerySubscription } from 'jazz-tools/svelte';import { app } from '../schema.js';const { id }: { id: string } = $props();const db = getDb();const todos = new QuerySubscription(() => app.todos.where({ id }).limit(1));const todo = $derived(todos.current?.[0]);</script>{#if todo}<li class={todo.done ? 'done' : ''}><inputtype="checkbox"checked={todo.done}onchange={() => db.update(app.todos, id, { done: !todo.done })}/><span>{todo.title}</span><button onclick={() => db.delete(app.todos, id)}>×</button></li>{/if}
src/TodoItem.tsximport { Show } from "solid-js";import { useAll, useDb } from "jazz-tools/solid";import { app } from "../schema.js";export function QuickstartItem(props: { id: string }) {const db = useDb();const todos = useAll(() => ({ query: app.todos.where({ id: props.id }).limit(1) }));const todo = () => todos.data?.[0];return (<Show when={todo()}>{(item) => (<li classList={{ done: item().done }}><inputtype="checkbox"checked={item().done}onChange={() => db().update(app.todos, props.id, { done: !item().done })}/><span>{item().title}</span><button onClick={() => db().delete(app.todos, props.id)}>×</button></li>)}</Show>);}
src/TodoItem.tsimport type { Db } from "jazz-tools";import { app as schemaApp, type Todo } from "../schema.js";export function renderTodoItem(todo: Todo, db: Db, app: typeof schemaApp) {const li = Object.assign(document.createElement("li"), {textContent: todo.title,});const toggle = Object.assign(document.createElement("input"), {type: "checkbox",checked: todo.done,onchange: () => db.update(app.todos, todo.id, { done: !todo.done }),});const remove = Object.assign(document.createElement("button"), {textContent: "\u00d7",onclick: () => db.delete(app.todos, todo.id),});li.prepend(toggle);li.append(remove);return 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.tsximport { 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 /></>);}
src/TodoList.vue<script setup lang="ts">import { useAll } from "jazz-tools/vue";import { app } from "../schema.js";import TodoItem from "./TodoItem.vue";import AddTodo from "./AddTodo.vue";const { data: todos } = useAll(app.todos);</script><template><ul><TodoItem v-for="todo in todos ?? []" :key="todo.id" :id="todo.id" /></ul><AddTodo /></template>
src/TodoList.svelte<script lang="ts">import { QuerySubscription } from 'jazz-tools/svelte';import { app } from '../schema.js';import TodoItem from './TodoItem.svelte';import AddTodo from './AddTodo.svelte';const todos = new QuerySubscription(app.todos);</script><ul>{#each todos.current ?? [] as todo (todo.id)}<TodoItem id={todo.id} />{/each}</ul><AddTodo />
src/TodoList.tsximport { For } from "solid-js";import { useAll } from "jazz-tools/solid";import { app } from "../schema.js";import { QuickstartItem } from "./quickstart-item.js";import { QuickstartAdd } from "./quickstart-add.js";export function QuickstartList() {const todos = useAll(() => ({ query: app.todos }));return (<><ul><For each={todos.data ?? []}>{(todo) => <QuickstartItem id={todo.id} />}</For></ul><QuickstartAdd /></>);}
src/main.tsconst list = document.getElementById("todos")!;db.subscribe(app.todos, (todos) => {list.replaceChildren(...todos.map((todo) => renderTodoItem(todo, db, app)));});
The callback receives a { all, delta } object — all is the current full result set, and delta is an array of row-level changes (each with a kind: added, removed, or updated).
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.tsimport { 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
- Authentication — local-first and external JWT auth
- Permissions — row-level access policies
- Queries — filtering, sorting, pagination, and relations
- Durability tiers — control when writes are confirmed
Example apps
- Todo app (React) — the app you just built, as a complete project
- Todo app (Solid) — the same app built with
jazz-tools/solid