Shared access between users

Grant other users access to your data using a shares table and existence-based permissions.

After user-owned data, the next pattern you're likely to need is sharing. This recipe shows how to let one user grant another access to specific rows using a shares table.

Schema

Add a todoShares table that records which user has access to which todo.

schema.ts
const schema = {
todos: s.table(
{
title: s.string(),
done: s.boolean(),
},
{ shares: s.reverse("todoShares", "todo") },
),
todoShares: s.table(
{
todoId: s.uuid(),
user_id: s.uuid(),
can_edit: s.boolean(),
},
{ todo: s.rel("todos", "todoId") },
),
};
​
type AppSchema = s.Schema<typeof schema>;
export const app: s.App<AppSchema> = s.defineApp(schema);

Permissions

The creator can do everything. Share recipients get read access, and optionally edit access via can_edit.

permissions.ts
s.definePermissions(app, ({ policy, anyOf, session }) => {
policy.todos.allowRead.where((todo) =>
anyOf([
{ "$createdBy.account": session.user.account },
policy.todoShares.exists.where({
todoId: todo.id,
user_id: session.user.account,
}),
]),
);
​
policy.todos.allowInsert.always();
​
policy.todos.allowUpdate.where((todo) =>
anyOf([
{ "$createdBy.account": session.user.account },
policy.todoShares.exists.where({
todoId: todo.id,
user_id: session.user.account,
can_edit: true,
}),
]),
);
​
policy.todos.allowDelete.where({ "$createdBy.account": session.user.account });
​
// Only the todo creator can manage shares
policy.todoShares.allowInsert.where((share) =>
policy.todos.exists.where({
id: share.todoId,
"$createdBy.account": session.user.account,
}),
);
policy.todoShares.allowRead.where({ user_id: session.user.account });
policy.todoShares.allowDelete.where((share) =>
policy.todos.exists.where({
id: share.todoId,
"$createdBy.account": session.user.account,
}),
);
});

See Permissions for more on exists.where, anyOf, and allOf.

Granting access

To share a todo, insert a row into todoShares.

export function shareTodo(
db: ReturnType<typeof useDb>,
todoId: string,
recipientAccountId: string,
) {
db.insert(app.todoShares, {
todoId,
user_id: recipientAccountId,
can_edit: false,
});
}

Querying shared items

SharedWithMe.tsx
export function SharedWithMe() {
const session = useSession();
const {
data: shares,
isLoading,
error,
} = useAll(
session?.user.account
? app.todoShares.where({ user_id: session.user.account }).include({ todo: true })
: undefined,
);
​
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Something went wrong!</p>;
​
return (
<ul>
{shares?.map((share) =>
share.todo ? (
<li key={share.id}>
{share.todo.title}
{share.can_edit ? " (can edit)" : " (read-only)"}
</li>
) : null,
)}
</ul>
);
}

Revoking access

Delete the share row to revoke access. The server will stop syncing the todo to the former recipient.

export function unshareTodo(db: ReturnType<typeof useDb>, shareId: string) {
db.delete(app.todoShares, shareId);
}