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.
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.
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.
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.