Real-time collaborative list

Multiple users subscribing to the same data and seeing each other's changes in real-time.

Every client subscribes to the same data and sees changes as they happen. This recipe shows the pattern end-to-end.

Schema

A shared project with collaboratively-edited tasks.

schema.ts
const schema = {
projects: s.table(
{
name: s.string(),
},
{
tasks: s.reverse("tasks", "project"),
members: s.reverse("projectMembers", "project"),
},
),
tasks: s.table(
{
title: s.string(),
done: s.boolean(),
assignee_id: s.uuid().optional(),
projectId: s.uuid(),
},
{ project: s.rel("projects", "projectId") },
),
projectMembers: s.table(
{
projectId: s.uuid(),
user_id: s.uuid(),
},
{ project: s.rel("projects", "projectId") },
),
};
​
type AppSchema = s.Schema<typeof schema>;
export const app: s.App<AppSchema> = s.defineApp(schema);

Permissions

Project members can read and write tasks. The creator can manage membership. Tasks inherit access from their project via allowedTo.

permissions.ts
s.definePermissions(app, ({ policy, anyOf, allowedTo, session }) => {
// Projects: creator and members
policy.projects.allowRead.where((project) =>
anyOf([
{ "$createdBy.account": session.user.account },
policy.projectMembers.exists.where({
projectId: project.id,
user_id: session.user.account,
}),
]),
);
policy.projects.allowInsert.always();
policy.projects.allowUpdate.where({ "$createdBy.account": session.user.account });
​
// Tasks: inherit from project
policy.tasks.allowRead.where(allowedTo.read("project"));
policy.tasks.allowInsert.where(allowedTo.read("project"));
policy.tasks.allowUpdate.where(allowedTo.read("project"));
​
// Members: only the creator can manage
policy.projectMembers.allowInsert.where((member) =>
policy.projects.exists.where({
id: member.projectId,
"$createdBy.account": session.user.account,
}),
);
policy.projectMembers.allowRead.where((member) =>
anyOf([
policy.projects.exists.where({
id: member.projectId,
"$createdBy.account": session.user.account,
}),
{ user_id: session.user.account },
]),
);
});

Subscribing to shared data

When multiple clients subscribe to the same query, they all see each other's changes in real-time.

ProjectTasks.tsx
export function ProjectTasks({ projectId }: { projectId: string }) {
const db = useDb();
const {
data: tasks,
isLoading,
error,
} = useAll(app.tasks.where({ projectId, done: false }).orderBy("$createdAt", "desc"));
​
function addTask(title: string) {
db.insert(app.tasks, { title, done: false, projectId });
}
​
function completeTask(taskId: string) {
db.update(app.tasks, taskId, { done: true });
}
​
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Something went wrong!</p>;
​
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<button onClick={() => completeTask(task.id)}>Done</button>
{task.title}
</li>
))}
</ul>
);
}

When Alice inserts a task it appears in her UI instantly, syncs to the server, and the server pushes it to Bob — whose useAll subscription re-renders automatically.

When two users edit the same row concurrently, Jazz uses last-writer-wins (LWW) per column. Each column resolves independently, so if Alice updates title while Bob updates done, both changes are preserved. If they both update title, Jazz's deterministic hybrid logical clock ordering decides the winner.

For most applications this is the right default. See How sync works for more detail.