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.

01 Global account state account disabled / banned → immediate deny 02 Server membership validation not a member → deny 03 Server default_permissions baseline allow/deny set for all members 04 Server roles (rank-ordered) lower rank = stronger; deny wins within layer 05 User attribute rows cross-server dimension; configured order 06 Channel default_permissions channel-level baseline; overrides server baseline 07 Channel role overrides (rank-ordered) same rank logic as server roles 08 Channel attribute overrides channel-scoped attribute-level overrides 09 Timeout mask ALLOW_IN_TIMEOUT set; overrides ordinary grants 10 Owner / privileged bypass explicit manifest set only; cannot manufacture unknown caps 11 Entitlement gating paid capabilities checked last; permissions can't create entitlement CapabilityDecision { allowed: boolean, reason: string } Later layers override earlier. Within one layer, deny wins.

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

Double Enforcement

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

Three Separate Systems

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.


Research Links

Final Recommendation (doc 00) → Shared TS Domain Architecture (doc 05) → Core Data Model Spec (doc 08) →