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 needed — Jazz tracks who created each row automatically via $createdBy.
const 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 to the session.user_id 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().
s.definePermissions(app, ({ policy, session }) => {
policy.todos.allowRead.where({ $createdBy: session.user_id });
policy.todos.allowInsert.always();
policy.todos.allowUpdate.where({ $createdBy: session.user_id });
policy.todos.allowDelete.where({ $createdBy: session.user_id });
});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.
export function MyTodos() {
const todos = useAll(app.todos.where({ done: false }));
if (!todos) return <p>Loading…</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.
export function AddTodo() {
const db = useDb();
function handleAdd(title: string) {
db.insert(app.todos, { title, done: false });
}
return <button onClick={() => handleAdd("Buy milk")}>Add</button>;
}