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, then by operation: read, history, mutate.new, mutate.existing, 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 acting subject — plus the policy-only predicates `{ teamScopeUsers: true }` and `{ approvalParty: true }`. There is no SQL escape; approvals and `authorize` are TS/Effect, never expression strings.

Field masks

  • Attribute-level — omitted from reads, hidden in UI, and refused on write: a submitted payload naming a masked field is rejected, never silently stripped.
  • Capabilities — apps, tools, MCP servers, and tenant-authored skills are granted on the policy; an unnamed tenant skill is unreachable, while the platform’s own skills are always available.

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 (commit under hold → seal / restore)
        └── no grant ──────────────────────────► denied
  1. Direct — grant matches; mutation proceeds
  2. Reduced — reads only: rows and fields narrowed to the grant’s `where` and `fields`. A write outside the mutation predicate or naming a masked field is refused, not reduced.
  3. Gated — the transform prepares the graph, then it is committed provisionally under a hold and an approval request tracks it
  4. Denied — an access-denied answer when no grant or approval path matches
Prepare, gate, then commit
Gated writes are prepared before review , but proposed values are not committed while review is open. A held create has no domain row; an existing update/delete target may carry `approval_id` while retaining its committed values. 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.