1# Permissions23`src/permissions.ts` is an author-owned security boundary. It defines which signed local-first4identity may read or mutate each table; hiding a button in the UI is not a substitute for policy.56## The starter policy78The generated task app uses creator-owned rows:910```ts11import { s } from "@nzip/lofi/schema";12import { app } from "./schema.ts";1314export default s.definePermissions(app, ({ policy, session }) => {15 policy.tasks.allowInsert.always();16 policy.tasks.allowRead.where({ $createdBy: session.user_id });17 policy.tasks.allowUpdate.where({ $createdBy: session.user_id });18 policy.tasks.allowDelete.where({ $createdBy: session.user_id });19});20```2122This means:2324- Any valid app session may create a task.25- A task is readable only by the identity that created it.26- Only that creator may update or delete it.27- Recovering the same identity restores the same authority; opening a new local account does not.2829## Adding a table3031Add all four decisions deliberately for each new table:32331. Who may insert?342. Who may read a row?353. Who may update it?364. Who may delete it?3738Start with creator ownership unless the product has a concrete collaboration model. Do not make a39table broadly readable merely to get a UI prototype working.4041## Changing an existing policy4243Treat a permission change like a data migration:44451. Describe the intended actors and operations in plain language.462. Update `src/permissions.ts`.473. Run `deno task schema:validate`.484. Run the application tests with at least one allowed and one rejected identity case.495. Deploy the schema/permission bundle explicitly with `deno task schema:deploy`.5051Managed sync configuration is required to publish schema and permission changes. Local-only mode is52still useful for UI work, but it does not prove a cloud permission policy was deployed correctly.5354## Choose one narrow access template5556The starter remains private by default. When a resource has a concrete collaboration model,57`@nzip/lofi/access` compiles three narrow templates through Jazz's own policy builder:5859| Template | Who can read | Who can mutate | Sync |60| --------------- | -------------------------------- | --------------------------------------------------- | -------- |61| `privateAccess` | Row creator | Row creator | Optional |62| `sharedAccess` | Creator plus explicit recipients | Creator; recipients with editable grants may update | Required |63| `groupAccess` | Group members | Determined by the fixed group role | Required |6465The schema stays on the lofi schema surface:6667```ts68import { s } from "@nzip/lofi/schema";69import { groupMembershipTable, sharedGrantTable } from "@nzip/lofi/access";7071const schema = {72 notes: s.table({ title: s.string() }),73 noteGrants: sharedGrantTable("notes"),74 workspaces: s.table({ name: s.string() }),75 workspaceMembers: groupMembershipTable("workspaces"),76 documents: s.table({ workspaceId: s.ref("workspaces"), title: s.string() }),77};78export const app = s.defineApp(schema);79```8081Then compose policies:8283```ts84export default defineAccessPolicies(app, [85 sharedAccess({ resource: app.notes, grants: app.noteGrants }),86 groupAccess({87 groups: app.workspaces,88 members: app.workspaceMembers,89 resources: app.documents,90 groupId: "workspaceId",91 }),92]);93```9495The templates do not replace the schema surface. Use `s.table()`, `s.defineApp()`, and96`s.definePermissions()` directly whenever they do not fit. `defineAccessPolicies` also accepts a97final raw-policy callback for a small extension without hiding the underlying policy builder.9899## Fixed group roles100101| Role | Read | Create | Edit own | Edit any | Manage members |102| ------------- | ---- | ------ | -------- | -------- | -------------- |103| `reader` | yes | no | no | no | no |104| `contributor` | yes | yes | yes | no | no |105| `writer` | yes | yes | yes | yes | no |106| `admin` | yes | yes | yes | yes | yes |107108The helper stores the role label plus fixed capability bits because the pinned Jazz alpha rejects109role-string comparisons inside relationship policies. Only the four role names are accepted by the110typed operations; custom roles belong in a raw policy callback.111112**The group creator holds a permanent superseat.** Whoever created the group row can always update113it and — because membership management derives from group-update authority — can always restore114their own admin membership, even after other admins demote or remove them. This is a documented115property of the template, not revocable admin membership: the pinned Jazz alpha cannot express116"creator authority only until the first admin exists" (its policy engine drops negated existence117conditions), so do not present creator handover as complete in product UI. The creator can also118delete their own group rows, which is what lets group creation roll back cleanly if bootstrapping119the first admin membership fails. See120[the decision record](decisions/group-creator-authority-alpha53.md).121122Direct shares and group membership use a non-secret `lofi1:<app-id>:<jazz-user-id>` sharing123identity. It is safe to copy for this purpose but is not a directory, invitation, or sign-in token.124The package deliberately does not discover users or deliver invitations.125126See the focused [Shared](examples/shared.md) and [Group](examples/group.md) examples and the127[access API reference](reference/access.md).128129When adding collaboration, test at least:130131- the creator's access;132- an explicitly authorized second identity;133- an unrelated identity;134- access after the product's revoke/remove action;135- offline edits followed by reconnection.