Permissions & Entitlements
One generated capability manifest. One pure-TypeScript layered calculator. Rank-ordered roles, cross-server user attributes, timeout mask, bounded owner bypass. Same code runs on client (prediction) and server (authority).
Permission Evaluation Layer Stack
Evaluation proceeds top-to-bottom through eleven layers. Later layers override earlier ones. Within a single layer, deny wins over allow.
The final output is a CapabilityDecision with a boolean result and a typed reason string.
Capability Manifest
All capabilities are declared in a single generated manifest. No capability can be granted at runtime that is not already in this manifest. This is the bounded set — the owner bypass can only reference members of this union type.
export type Capability =
| "server.manage"
| "channel.view"
| "channel.manage"
| "message.send"
| "message.moderate"
| "thread.create"
| "course.view"
| "course.manage"
| "stream.join"
| "stream.start"
| "server.billing.manage";
CapabilityDecision Type
Every evaluation call returns a CapabilityDecision. The reason field is a typed discriminant —
never a free-form string — so callers can branch on specific denial reasons in both product UI and audit logging.
export type CapabilityDecision = {
allowed: boolean;
reason:
| "allowed"
| "not-member"
| "role-denied"
| "channel-denied"
| "missing-entitlement"
| "timed-out"
| "server-suspended";
};
Evaluation Order
| Step | Layer | Rule |
|---|---|---|
| 1 | Global account | Disabled or banned account → immediate deny. Short-circuits all subsequent evaluation. |
| 2 | Server membership | Actor must have an active server_memberships row. Not a member → deny. |
| 3 | Server defaults | Baseline allow/deny set from servers.default_permissions. Applies to every member. |
| 4 | Roles (rank-ordered) | Lower rank number = stronger role. All the actor's roles are evaluated in rank order. Deny wins within the same rank. |
| 5 | User attributes | Cross-server dimension; attribute rows apply in configured priority order. Enables cross-server verified badges to affect permissions. |
| 6 | Channel defaults | Channel-level baseline from channels.default_permissions. Overrides server baseline for this channel. |
| 7 | Channel roles | Same rank logic as server roles, applied through channel_role_overrides. Deny wins within rank. |
| 8 | Channel attributes | Channel-scoped attribute overrides from channel_member_overrides. Finest-grained row-level control. |
| 9 | Timeout mask | If actor is timed out, only the ALLOW_IN_TIMEOUT explicit set applies. All other grants are suppressed. |
| 10 | Owner bypass | Explicitly enumerated capabilities from the manifest. Cannot manufacture a capability not in the manifest. Bounded bypass. |
| 11 | Entitlements | Paid capabilities are checked last against the effective entitlements projection. A permission grant cannot create an entitlement. |
Sync Visibility
Permission enforcement happens at two independent points: Zero query definitions only sync rows the actor may currently read (enforced at publication time), and mutators independently authorize every write (enforced at mutation time). Neither point is optional.
When access is revoked — a user is removed from a server, a role is deleted, or a channel override is changed — the sync engine removes inaccessible rows from the local client replica. Revocation propagation is explicitly tested in the viability spike. The client cannot retain stale rows it should no longer see.
Key Decisions
Permission (may the actor perform this action?) is separate from Entitlement (does the actor have this paid capability?) is separate from Feature flag (is this actor in the cohort for this rollout?). These three must not be collapsed into a single flag system. Collapsing them creates authorization drift, billing inconsistencies, and untestable rollout logic.
- Bitsets are derived caches only. The
default_permissionsbitset column is a performance cache derived from the canonicalcapability_key/effectrows. Rows are the authority; bitsets are never written directly by product logic. - One calculator, two environments. The pure-TypeScript layered calculator runs identically on the client (for UI prediction) and on the server (for authoritative mutation checks). No divergence between client and server permission logic.
- Deny wins within a layer. When multiple roles at the same rank grant conflicting effects on the same capability, deny wins. This is not configurable — it is a fundamental property of the algebra.
- The owner bypass is bounded. Server owners can bypass all ordinary permission layers, but only for capabilities explicitly named in the manifest. The calculator cannot generate a capability that does not exist in the manifest type.