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.tsconst 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.tss.definePermissions(app, ({ policy, anyOf, allowedTo, session }) => {// Projects: creator and memberspolicy.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 projectpolicy.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 managepolicy.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.tsxexport 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.