User-owned data
End-to-end walkthrough of the most common Jazz pattern: data that belongs to the user who created it.This recipe covers building an app where users own their own data and no-one else can see it, covering schema, permissions, querying, and inserting.
Schema
There's no need to add an explicit owner column — Jazz tracks who created each row automatically via $createdBy.
schema.tsconst schema = {todos: s.table({title: s.string(),done: s.boolean(),},{},),};type AppSchema = s.Schema<typeof schema>;export const app: s.App<AppSchema> = s.defineApp(schema);
See Defining tables for the full schema DSL.
Permissions
Match $createdBy.account to session.user.account to validate whether the current user is the one who created the data. Because $createdBy is set automatically, we can declare insert explicitly with .always().
permissions.tss.definePermissions(app, ({ policy, session }) => {policy.todos.allowRead.where({ "$createdBy.account": session.user.account });policy.todos.allowInsert.always();policy.todos.allowUpdate.where({ "$createdBy.account": session.user.account });policy.todos.allowDelete.where({ "$createdBy.account": session.user.account });});
These rules are enforced on the server. See Permissions for combinators, allowedTo, and more complex options.
Querying
The table's permissions already scope results to the current user, so queries don't need a separate owner filter.
MyTodos.tsxexport function MyTodos() {const { data: todos, isLoading, error } = useAll(app.todos.where({ done: false }));if (isLoading) return <p>Loading…</p>;if (error) return <p>Something went wrong!</p>;return (<ul>{todos.map((todo) => (<li key={todo.id}>{todo.title}</li>))}</ul>);}
See Queries for subscriptions, one-shot queries, and durability tiers.
Inserting
$createdBy is set automatically on insert, so we don't need to set an owner.
AddTodo.tsxexport function AddTodo() {const db = useDb();function handleAdd(title: string) {db.insert(app.todos, { title, done: false });}return <button onClick={() => handleAdd("Buy milk")}>Add</button>;}
Use db.insert(...).wait({ tier: "..." }) if you need confirmation that the write reached a specific durability tier.
If ownership can be transferred after creation, use an explicit owner_id column instead of $createdBy.
schema.tsconst schemaExplicit = {todos: s.table({title: s.string(),done: s.boolean(),owner_id: s.uuid(),},{},),};type ExplicitAppSchema = s.Schema<typeof schemaExplicit>;export const explicitApp: s.App<ExplicitAppSchema> = s.defineApp(schemaExplicit);
permissions.tss.definePermissions(explicitApp, ({ policy, session }) => {policy.todos.allowRead.where({ owner_id: session.user.account });policy.todos.allowInsert.always();policy.todos.allowUpdate.whereOld({ owner_id: session.user.account });policy.todos.allowDelete.where({ owner_id: session.user.account });});
allowUpdate.whereOld(...) checks the row before the update, so the current owner can rewrite owner_id to transfer the row. Using .where(...) instead would also enforce the condition on the post-update row and block transfers. See Permissions for whereOld/whereNew semantics.
allowInsert.always() lets any user insert a row with any owner_id, including someone else's. That's the right default if you want users to be able to assign rows to others on creation; otherwise, narrow it to .where({ owner_id: session.user.account }) so clients can only create rows they own.