Data Patterns

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.

On this page