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.table({
    title: s.string(),
    done: s.boolean(),
    assignee_id: s.string().optional(),
    projectId: s.ref("projects"),
  }),
  projectMembers: s.table({
    projectId: s.ref("projects"),
    user_id: s.string(),
  }),
};

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: session.user_id },
      policy.projectMembers.exists.where({
        projectId: project.id,
        user_id: session.user_id,
      }),
    ]),
  );
  policy.projects.allowInsert.always();
  policy.projects.allowUpdate.where({ $createdBy: session.user_id });

  // Tasks: inherit from project
  policy.tasks.allowRead.where(allowedTo.read("projectId"));
  policy.tasks.allowInsert.where(allowedTo.read("projectId"));
  policy.tasks.allowUpdate.where(allowedTo.read("projectId"));

  // Members: only the creator can manage
  policy.projectMembers.allowInsert.where((member) =>
    policy.projects.exists.where({
      id: member.projectId,
      $createdBy: session.user_id,
    }),
  );
  policy.projectMembers.allowRead.where((member) =>
    anyOf([
      policy.projects.exists.where({
        id: member.projectId,
        $createdBy: session.user_id,
      }),
      { user_id: session.user_id },
    ]),
  );
});

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 tasks = 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 (!tasks) return <p>Loading…</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.

On this page