Skip to main content

Permission Profiles

Permission Rule Profiles are reusable RBAC bundles you assign to memberships (or to API keys via Integrations). Each profile is an ordered list of rules; the middleware walks them last-match-wins on every authenticated request.

Find the admin at Admin → Permission Profiles. Route: /admin/permission-profiles.

The rule grammar

A rule is one string. It can be one of:

FormWhat it matches
*Catch-all — matches every entity-op pair
read:* / write:*Op-only — matches every entity for that op
<op>:<Entity>Exact match. Wildcards on either side: *:Setup, read:*Sheet
<HTTP>:/<path-glob>HTTP-style. Inert against the in-process entity-op gate (returns false). Useful for documenting an intent that an external API gateway can mirror without affecting the in-process check.

Each rule is prefixed with + (allow) or - (deny). For example:

+ *
- write:Setup
+ read:Issue

Walks last-match-wins:

  • read:Lap → matched by + * → allow
  • write:Setup → matched by - write:Setup → deny
  • read:Issue → last match is + read:Issue → allow

If no rule matches, the default is allow. Owners and admins always pass.

The middleware

apps/api/src/middleware/permissions.ts is the in-process gate. Routes call requirePermission("Setup", "write") (or use the factory) and the middleware:

  1. Resolves the caller's profile via their membership
  2. Walks the rule list last-match-wins
  3. Returns 403 on deny, passes through on allow

Profile-to-membership lookups are cached in-process by (accountId, userId). The cache key is invalidated on profile mutation and on assignment changes.

The admin

Master/detail layout:

  • Left — profile list (with a "Read Only" + "Full Access" pair seeded for every account)
  • Right — rule editor with one row per rule (effect / pattern), drag to reorder, an explanatory pane showing how the current ruleset evaluates against a sample (entity, op) pair

Assigning a profile

Profiles attach to memberships via POST /memberships/:id/permission-profile. Surfaced in the Team admin (/admin/team) — pick a profile from the dropdown next to each member's row.

API keys can pin their own profile at create time on POST /api-keys — that profile takes precedence over the key owner's membership profile.

What you can do today

  • Author profiles with the rule grammar above
  • Assign profiles to memberships and API keys
  • Use the seeded Read Only profile to validate the path
  • 10/10 vitest cases passing on the matcher

What's coming

  • Visual rule builder — today rules are typed strings; a guided builder is queued
  • Per-entity-id rules — today the rules are entity-type scoped only
  • Audit log — record every denied request with the matched rule for debugging