Access Control
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.
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.
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
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);
}