Raxx · internal docs

internal · gated ↑ index

Raxx RBAC — Role-Based Access Control Design (v1, First Pass)

Status: Draft — first pass; multi-pass expected
Owner: software-architect
Last updated: 2026-04-25
Branch: design/rbac-v1-first-pass
Related ADRs: 0020
Refs: Console flat RBAC in console/app/middleware/rbac.py; ADR-0001, ADR-0002, ADR-0003


1. Context

Raxx today has a flat, four-level role column on console_admins (superadmin, ops, support, readonly) and no RBAC on Antlers (user-facing app) whatsoever. As the platform grows — multiple apps, an operator console, a secrets vault, a marketing surface, a mobile client — the flat model cannot express the permissions needed without either over-granting or hard-coding dozens of booleans.

This document specifies the shape of the target RBAC model: per-app fine-grained roles, group-mediated permission assignment, a DAG-based inheritance structure, and a data model that can survive intact when Raxx adopts SAML.

This is a design exploration, not a final spec. Edge cases and open questions are called out explicitly in §9.


2. Invariants

The following constraints are non-negotiable and override any design choice below:

  1. No stored credentials. Nothing in this RBAC model may store a password, recovery code, TOTP seed (outside the existing encrypted-seed pattern in the Console), or any value that could replay an authentication.
  2. Passkeys / WebAuthn only as auth factor. RBAC determines what an authenticated session may do; it does not change how authentication happens.
  3. No direct permissions on users. Permissions flow exclusively: user → groups → roles → permissions. A permission check against a bare user ID must not exist.
  4. Superadmin is break-glass only. It is never assigned as a working role. A human in a break-glass group must use a named role for day-to-day tasks.
  5. Every state-changing action that affects money, permissions, or data access writes an audit row.
  6. GDPR by default. Any PII stored in the RBAC data model (email, display name) follows the same retention, erasure, and portability rules as the rest of the platform.
  7. Credentials into infra, not into code. No RBAC secret (signing key, DB URL) appears in files that ship.

3. Core Principles

3.1 Naming convention

All roles follow the pattern <app>-<resource>-<level> or <app>-<level> for app-wide roles:

console-token-user          # can read tokens in the console
console-token-admin         # inherits console-token-user; can also mutate tokens
console-user                # general console access — does NOT include console-token-user
console-admin               # inherits console-user; app-wide admin
antlers-user                # end user, free tier
antlers-founders            # Founders trial tier
antlers-pro                 # paid Pro tier
antlers-support-readonly    # support agent; read access to user-facing surfaces
raptor-admin                # API admin operations
vault-reader                # read secrets from Infisical (mapping layer — see §3.3)
vault-admin                 # rotate + create secrets in Infisical
getraxx-editor              # can modify marketing content (future)

3.2 Deliberate non-inheritance

console-admin does NOT automatically carry console-token-admin. The inheritance graph only flows within a resource family (console-token-admin → console-token-user). Crossing resource families requires explicit group composition. This is intentional: it prevents a promotion on one resource from silently granting power on an unrelated one.

3.3 Vault is a mapping layer

Infisical has its own native RBAC. Raxx does not redesign it. Instead, Raxx defines local roles (vault-reader, vault-admin) whose permissions map to Infisical Machine Identity scopes. The local permission check determines whether the Raxx system issues a Infisical API call on behalf of the requesting session, not whether the session calls Infisical directly.


4. Role Taxonomy

4.1 Console (console.raxx.app)

Role Inherits Description
console-token-user Read tokens in the rotation UI
console-token-admin console-token-user Rotate, create, delete tokens; manage rotation SOPs
console-secrets-user View secrets index (Infisical read via vault-reader)
console-secrets-admin console-secrets-user Trigger vault writes; manage Infisical mapping
console-audit-user View audit log
console-invite-admin Send admin invitations
console-env-admin Switch environment context (prod / staging)
console-flag-admin Toggle feature flags
console-user Dashboard, read-only views. Does NOT auto-include any resource-level role.
console-manager console-user Manage console configurations (flags, env, invites). Composition of console-flag-admin, console-env-admin, console-invite-admin.
console-ops console-user, console-audit-user Operator daily driver. Add resource roles explicitly.

Mapping from the current flat model (migration in §8):

Old role New group (suggested)
readonly group with console-user
support group with console-user, console-audit-user, antlers-support-readonly
ops group with console-ops, console-token-admin, console-secrets-user, console-flag-admin
superadmin break-glass group only; no working use

4.2 Antlers (raxx.app) — product-facing

Role Inherits Description
antlers-user Authenticated end user, free tier
antlers-founders antlers-user Founders trial access (gated by paper-first engine)
antlers-pro antlers-founders Paid Pro tier
antlers-support-readonly Operator read into user-facing surfaces (support agent)
antlers-org-admin antlers-pro Future: team/org admin (when teams ship)

Antlers has no RBAC code today. This taxonomy is prospective; implementation is a follow-up milestone.

4.3 Raptor (api.raxx.app)

Role Inherits Description
raptor-admin Admin endpoints (/api/admin/*); issued via console JWT
raptor-read Read-only admin access

Raptor's user model inherits Antlers' user roles (same identity DB). Operator access to Raptor admin endpoints uses the existing CONSOLE_RAPTOR_JWT_SECRET pattern (short-lived JWT from console). The RBAC layer governs which console role may trigger which Raptor admin call.

4.4 Getraxx (marketing site)

Minimal; mostly anonymous public. One operator role:

Role Description
getraxx-editor Publish or modify marketing content (future CMS integration)

4.5 Vault (Infisical mapping)

Local Role Infisical scope equivalent
vault-reader Read secrets in /MooseQuest/*
vault-admin Read + write + rotate in /MooseQuest/*

5. Inheritance Graph

graph TD
    subgraph "Console — token resource"
        CT_U["console-token-user"]
        CT_A["console-token-admin"] --> CT_U
    end

    subgraph "Console — secrets resource"
        CS_U["console-secrets-user"]
        CS_A["console-secrets-admin"] --> CS_U
    end

    subgraph "Console — app-wide"
        C_U["console-user"]
        C_MGR["console-manager"] --> C_U
        C_OPS["console-ops"] --> C_U
        C_OPS --> C_AUD["console-audit-user"]
    end

    subgraph "Antlers — product tiers"
        A_U["antlers-user"]
        A_F["antlers-founders"] --> A_U
        A_P["antlers-pro"] --> A_F
    end

    subgraph "Vault mapping"
        V_R["vault-reader"]
        V_A["vault-admin"] --> V_R
    end

    %% Explicit NON-inheritance callouts (dashed = does NOT flow)
    C_MGR -. "does NOT include" .-> CT_U
    C_MGR -. "does NOT include" .-> CS_U
    C_OPS -. "does NOT include" .-> CT_U
    C_OPS -. "does NOT include" .-> CS_U
    A_P   -. "does NOT include" .-> C_U

Key deliberate non-inheritance edges: - console-manager and console-ops do not carry console-token-user or console-secrets-user. A support agent or ops manager is not implicitly a token-handler. - antlers-pro does not carry any console-* role. Product and operator surfaces are entirely separate. - No console-* role inherits any antlers-* role and vice versa.


6. Group Composition Examples

graph LR
    subgraph "raxx-platform-admins"
        PA_CT_A["console-token-admin"]
        PA_CS_A["console-secrets-admin"]
        PA_C_M["console-manager"]
        PA_C_A["console-audit-user"]
        PA_VA["vault-admin"]
        PA_RA["raptor-admin"]
    end

    subgraph "raxx-support-team"
        ST_CU["console-user"]
        ST_CA["console-audit-user"]
        ST_AS["antlers-support-readonly"]
        ST_RR["raptor-read"]
    end

    subgraph "raxx-billing-team (future)"
        BT_C["console-user"]
        BT_B["billing-reader (TBD)"]
    end

    subgraph "raxx-founders-cohort"
        FC_AF["antlers-founders"]
    end

    subgraph "break-glass (Kristerpher bootstrap)"
        BG_ALL["all roles — emergency only"]
    end
Group Roles
raxx-platform-admins console-token-admin, console-secrets-admin, console-manager, console-audit-user, vault-admin, raptor-admin
raxx-support-team console-user, console-audit-user, antlers-support-readonly, raptor-read
raxx-billing-team console-user, billing-reader (role TBD when billing ships)
raxx-founders-cohort antlers-founders
break-glass all roles; single member (Kristerpher); requires step-up WebAuthn + audit event; session duration capped at 2h

7. Data Model

All RBAC tables live in the Console Postgres (identity-authoritative DB). Antlers and Raptor call the Console via short-lived JWT for permission checks, or the permission resolution result is embedded in the session token. Per-app data stays in per-app DBs; RBAC metadata lives in one place.

See ADR-0020 for the decision rationale (centralized identity vs per-app).

-- Users are managed in the existing console_admins table (operators)
-- and Raptor's users table (product users). The RBAC tables below apply
-- to operators (Console identity DB). Product-user roles (antlers-*)
-- are stored as a tier column on Raptor's users table for v1 and will
-- be migrated to full RBAC in a follow-up milestone.

CREATE TABLE groups (
    id          TEXT PRIMARY KEY,          -- uuid v4
    name        TEXT UNIQUE NOT NULL,      -- e.g. 'raxx-platform-admins'
    description TEXT,
    created_at  TIMESTAMP NOT NULL,
    updated_at  TIMESTAMP NOT NULL
);

CREATE TABLE roles (
    id          TEXT PRIMARY KEY,          -- uuid v4
    name        TEXT UNIQUE NOT NULL,      -- e.g. 'console-token-admin'
    app         TEXT NOT NULL,             -- 'console' | 'antlers' | 'raptor' | 'vault' | 'getraxx'
    description TEXT,
    created_at  TIMESTAMP NOT NULL
);

CREATE TABLE permissions (
    id          TEXT PRIMARY KEY,          -- uuid v4
    name        TEXT UNIQUE NOT NULL,      -- e.g. 'console:tokens:rotate'
    description TEXT,
    created_at  TIMESTAMP NOT NULL
);

-- Role → Permission (M2M)
CREATE TABLE role_permissions (
    role_id       TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, permission_id)
);

-- Role inheritance DAG (M2M; child inherits parent's permissions)
-- "console-token-admin inherits console-token-user"
-- → child_role_id = console-token-admin, parent_role_id = console-token-user
CREATE TABLE role_inheritance (
    child_role_id  TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    parent_role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (child_role_id, parent_role_id),
    CHECK (child_role_id <> parent_role_id)
);

-- Group → Role (M2M)
CREATE TABLE group_roles (
    group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    role_id  TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (group_id, role_id)
);

-- User → Group (M2M; operator users only in Console DB)
CREATE TABLE user_groups (
    user_id  TEXT NOT NULL REFERENCES console_admins(id) ON DELETE CASCADE,
    group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, group_id)
);

-- Indexes
CREATE INDEX idx_user_groups_user    ON user_groups(user_id);
CREATE INDEX idx_group_roles_group   ON group_roles(group_id);
CREATE INDEX idx_role_perms_role     ON role_permissions(role_id);
CREATE INDEX idx_role_inh_child      ON role_inheritance(child_role_id);

What is deliberately absent: any column storing a credential, password, TOTP seed, or recovery token in these tables. The CI grep from ADR-0002 extends to cover RBAC migrations.


8. Permission Resolution Algorithm

Per-request check for can user U perform permission P?:

1. SELECT group_id FROM user_groups WHERE user_id = U
2. SELECT role_id FROM group_roles WHERE group_id IN (step 1 results)
3. Walk role_inheritance DAG: for each role R, union R with all ancestors
   → result: expanded_roles (set)
4. SELECT permission_id FROM role_permissions WHERE role_id IN expanded_roles
   → result: effective_permissions (set)
5. Return P ∈ effective_permissions

DAG cycle guard: the role_inheritance table enforces child_role_id <> parent_role_id. Application-layer must check for cycles before inserting a new inheritance edge (topological sort). A cycle in the DAG is a 422 at the admin API, not a runtime failure.

Caching strategy: Walking the full DAG on every request is too expensive. Recommended approach (decision deferred to feature-developer within these bounds):

v1 recommendation: Option A (session-embedded). Migrate to B if the console grows to >100 operators or sub-second latency matters for automated tooling.


9. SAML Readiness

v1 uses local group/role assignment (operator invites, console UI). The schema is designed so that the same group table is the join target for SAML-provided group memberships.

Flow when SAML lands:

  1. SAML assertion carries groups: ["raxx-platform-admins", "raxx-support-team"] (IdP emits group names, not role names — IdP does not need to know Raxx role taxonomy).
  2. On SSO login, Raxx looks up each group name in groups.name. If found, adds to user_groups for the session lifetime (or persists, depending on policy decision in §10 open question #3).
  3. If a SAML group name does not exist in groupsfail-closed. The session is created but carries zero RBAC. The admin sees a "no permissions assigned" screen and the system alerts ops@raxx.app. Auto-creating unknown groups from SAML assertions is a privilege-escalation risk.
  4. Bootstrap path for existing admins (e.g., Kristerpher): local groups are the source of truth until SAML is enabled. On SAML cutover, a one-time migration script maps existing console_admins.role values to group memberships. The script is idempotent and requires manual review before run.

IdP integration note: IdP emits group names only. The mapping from IdP group names to Raxx groups must be maintained in the Console admin UI (a group can have an idp_group_name column added in a later migration). Role names never leave Raxx's internal schema.


10. Operator-Only vs Product-Facing

Surface Visible to customers Managed by whom
Antlers user-tier groups (antlers-user, antlers-founders, antlers-pro) Yes (tier is user-visible) System (automated on payment/signup events)
Console operator groups (raxx-platform-admins, raxx-support-team, etc.) No Console console-invite-admin role
Break-glass group No Kristerpher directly; requires step-up
Vault mapping roles (vault-reader, vault-admin) No Console console-secrets-admin role

Overlap case — support agent with product visibility: A support agent who needs to read user-facing surfaces AND operator views belongs to both raxx-support-team (operator) and has antlers-support-readonly (product-read) in that group. No single role spans both surfaces.


11. Migration Path from Current State

Current state: console_admins.role TEXT CHECK IN ('superadmin','ops','support','readonly').

Target state: console_admins has no role column; membership in user_groups determines everything.

Migration steps:

  1. Add groups, roles, permissions, role_permissions, role_inheritance, group_roles, user_groups tables (new migration: 0002_rbac_tables.sql).
  2. Seed the role taxonomy from §4 via a data migration script (scripts/db/seed_rbac_roles.py).
  3. Seed the four legacy groups (legacy-readonly, legacy-support, legacy-ops, break-glass) and assign roles per the mapping table in §4.1.
  4. For each existing console_admins row, insert into user_groups for the matching legacy group.
  5. Update rbac.py middleware to resolve permissions via the new tables rather than admin.role string comparison.
  6. Run smoke tests: all existing routes still pass with the migrated data.
  7. In a separate PR: remove console_admins.role column (once all callers updated).

Rollback: Steps 1–4 are additive. Rolling back means dropping the new tables and reverting rbac.py to the string-comparison model. The role column is not removed until step 7, so rollback before that point is clean.

Antlers RBAC: No RBAC code exists today. The users.role column ('user'|'admin') is retained for now. Full Antlers RBAC (group membership, fine-grained roles) is a follow-up milestone. The schema above is designed to accommodate Raptor/Antlers users in a future migration by adding a parallel antlers_user_groups table or extending user_groups to reference a unified identity.


12. Rollout Plan

Phase Description
Dark New tables exist; rbac.py still uses old role column. Tests cover both paths.
Flag-on (staging) RBAC_V2=1 enables new resolution path in staging only. Legacy groups shadow old roles 1:1.
Beta (prod, read-only) New path active for read operations; mutations fall back to old path.
GA Old role column deprecated; new path for all operations. Remove legacy groups after 30-day soak.
Cleanup Drop console_admins.role column.

13. Security Considerations


14. Open Questions (require Kristerpher's judgment)

  1. Antlers RBAC timeline. Product-tier roles (antlers-user, antlers-founders, antlers-pro) are prospective here. Should they be backed by the full group/role model from day one, or should the users.role tier column stay in place until teams/orgs ship? This affects the migration scope for Raptor.
  2. Single identity DB vs per-app. This design puts the RBAC authority in Console Postgres. Product users (Antlers/Raptor) check permissions by calling a Console identity endpoint. Alternatively, Raptor maintains its own user RBAC tables and syncs with Console. Decision needed before implementing Antlers RBAC (the Console-authoritative approach is proposed in ADR-0020; flag if you want to reconsider).
  3. SAML group-name collision policy. If an IdP-provided group name matches a Raxx group name by accident during a future SAML migration, the user silently gets those permissions. Should Raxx prefix IdP-sourced groups (e.g., idp::raxx-platform-admins) to isolate them from locally-managed groups? This is the safest option but requires an IdP-to-local mapping layer.
  4. Break-glass session cap. Proposal: 2-hour fixed session for break-glass group members. Is 2 hours sufficient for a real incident, or should this be configurable?
  5. Role deprovisioning on suspension. When a console_admins row is suspended (not deleted), should user_groups rows be removed immediately (hard cut) or retained and ignored while status = suspended? Retaining makes reinstatement easier; removing is more secure.
  6. console-manager vs console-ops split. Kristerpher named console-manager as the "account that manages configurations." Today's ops role covers configurations and operations. Is the split intentional (manager = config-only, ops = operational actions)? If yes, the role matrix in §4.1 holds. If no, collapse them.
  7. Billing roles. raxx-billing-team and a billing-reader role are stubbed as TBD. Should billing RBAC wait for the billing epic, or should the schema reserve the role name now to prevent a naming conflict?

End of first-pass design. See ADR-0020 for the centralized-identity decision. Sub-cards to be filed.