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(),
  }),
  todoShares: s.table({
    todoId: s.ref("todos"),
    user_id: s.string(),
    can_edit: s.boolean(),
  }),
};

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: session.user_id },
      policy.todoShares.exists.where({
        todoId: todo.id,
        user_id: session.user_id,
      }),
    ]),
  );

  policy.todos.allowInsert.always();

  policy.todos.allowUpdate.where((todo) =>
    anyOf([
      { $createdBy: session.user_id },
      policy.todoShares.exists.where({
        todoId: todo.id,
        user_id: session.user_id,
        can_edit: true,
      }),
    ]),
  );

  policy.todos.allowDelete.where({ $createdBy: session.user_id });

  // Only the todo creator can manage shares
  policy.todoShares.allowInsert.where((share) =>
    policy.todos.exists.where({
      id: share.todoId,
      $createdBy: session.user_id,
    }),
  );
  policy.todoShares.allowRead.where({ user_id: session.user_id });
  policy.todoShares.allowDelete.where((share) =>
    policy.todos.exists.where({
      id: share.todoId,
      $createdBy: session.user_id,
    }),
  );
});

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, recipientUserId: string) {
  db.insert(app.todoShares, {
    todoId,
    user_id: recipientUserId,
    can_edit: false,
  });
}

Querying shared items

SharedWithMe.tsx
export function SharedWithMe() {
  const session = useSession();
  const shares = useAll(
    app.todoShares.where({ user_id: session!.user_id }).include({ todo: true }),
  );

  if (!shares) return <p>Loading…</p>;

  return (
    <ul>
      {shares.map((share) => (
        <li key={share.id}>
          {share.todo.title}
          {share.can_edit ? " (can edit)" : " (read-only)"}
        </li>
      ))}
    </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);
}

On this page