Defining Tables
Define tables and relationships in schema.ts using the TypeScript DSL.
Project layout
A Jazz project has a small set of files at the app root:
app-root/
├── schema.ts # Structural schema — tables and columns
├── permissions.ts # Optional row-level policies
└── migrations/ # Reviewed migration edges
└── 20260331-add-description-aaa-bbb.tsschema.ts is the source of truth for your data model. permissions.ts is optional and must be a
separate file. The migrations/ directory holds reviewed migration stubs — see
Migrations for the full workflow.
Table definitions
Tables are defined in schema.ts using the Jazz DSL. Each s.table(...) call registers a table and s.ref(...) defines typed relations between them.
projects: s.table({
name: s.string(),
}),
todos: s.table({
title: s.string(),
done: s.boolean(),
description: s.string().optional(),
owner_id: s.string().optional(),
parentId: s.ref("todos").optional(),
projectId: s.ref("projects").optional(),
}),s.ref() columns must be named with an Id or _id suffix (for example projectId or
owner_id). For s.array(s.ref()), use an Ids or _ids suffix instead. The runtime enforces
this convention and will throw if a ref column name does not match.
Validate locally
Validate your schema and permissions locally:
pnpm dlx jazz-tools@alpha validate
This validates schema.ts and the optional permissions.ts, then compiles them into Jazz's
internal schema representation.
When Jazz reports a difference between the old and new schema hashes after changing schema.ts, and your app already has data, you should create and push a migration edge as described in Migrations:
pnpm dlx jazz-tools@alpha migrations create <appId> --fromHash <fromHash> --toHash <toHash>
pnpm dlx jazz-tools@alpha migrations push <appId> <fromHash> <toHash>
When you change your schema on a shared app, create and push a migration. See Migrations for details.
If you need to clear local browser data after a schema change, see How do I reset browser storage?.
Exporting the app
s.defineApp(schema) converts your schema definition into a typed app object. This is what you
pass to queries, mutations, and subscriptions throughout your application code.
type AppSchema = s.Schema<typeof schema>;
export const app: s.App<AppSchema> = s.defineApp(schema);
export type Todo = s.RowOf<typeof app.todos>;The app object has one typed table handle per table (e.g. app.todos, app.projects). Table handles are query builders — you chain .where(), .include(), .orderBy() and other methods directly on them.
Type helpers
Extract precise TypeScript types from any table handle:
| Helper | Returns |
|---|---|
s.RowOf<typeof app.todos> | The row type (all columns, id included) |
s.InsertOf<typeof app.todos> | The insert shape (no id, respects optionals and defaults) |
s.WhereOf<typeof app.todos> | The where(...) input shape for that table |