Skip to content

Policies

Policies

Policies are how an operation controls who can see and change what. One policy, one file: the grants themselves (per collection, per action, with row `where` and field masks), the capabilities the holder may call, the rate limits, and the approval on a grant. Everything here is enforced on the server — permissions, not placeholders.

The policy model

Access is authored as reusable policies — never per-user rows:

  • policy — one file: description, grants{ collection: { read?, history?, create?, update?, delete? } }
  • teams — the team map: which policies each named team holds Team rows (name, parent, description) come from the dashboard; authority comes from src/access/+teams.ts , bound by name

is object-keyed by collection and action: read, history, create, update, delete
Holders: a person through their team, an envoy, or an automation
. A team row the map does not name is inert. Org admins bypass policy evaluation.

Row-level conditions

A read or history grant takes where and fields ; the where uses tokens such as ${requestor.id} . Partial matches produce reduced access — row filters merged into the SQL WHERE clauses of every read and mutation, so a user simply cannot see or touch rows outside their grants.

Token vocabulary: ${requestor.id} ${requestor.team_scope_users} — the requestor along the whole path — and `$sql` for predicates a comparison cannot express. Approvals, step functions, and `authorize` are TS/Effect, never expression strings.

Field masks

  • Attribute-level — omitted from reads, hidden in UI, and stripped from submitted payloads.
  • Capabilities — apps, tools, MCP servers, and skills are granted on the policy; something not named is not reachable.

What a mutation can do

  create | update | delete
        │
        ▼
  resolve the subject's grants for this collection/action
        │
        ├── grant matches ─────────────────────► direct
        ├── grant narrows (where/fields) ──────► reduced (SQL + mask)
        ├── grant carries an approval ────────► gated (write-then-lock)
        └── no grant ──────────────────────────► denied
  1. Direct — grant matches; mutation proceeds
  2. Reduced — rows and fields narrowed to the grant’s `where` and `fields`
  3. Gated — the write applies immediately, the records are locked with the request id, and an approval request is raised (write-then-lock)
  4. Denied — an access-denied answer when no grant or approval path matches
Write-then-lock, not block-until-approved
Gated writes are not held in a queue. The change applies while the review is open, and the records stay locked (`approval_id`) until an approve releases them. The full lifecycle — decisions, supersede, withdraw, conflict — is in Approval workflows.
  1. Name the teams in `src/access/+teams.ts`; teams are what approvers and holders use.
  2. Author one policy per surface, with grants per collection.
  3. Hold policies by team; envoys and automations name their own.
  4. Add approvals only on mutations that need review.
  5. Test each role with its own preview account before go live.