Raxx · internal docs

internal · gated ↑ index

ADR-0020: RBAC — Groups as the permission bridge; centralized identity authority

Date: 2026-04-25
Status: Proposed
Decider: software-architect (pending Kristerpher confirmation)
Refs: docs/architecture/rbac-design.md, ADR-0001, ADR-0002, ADR-0003


Context

Raxx needs a role-based access control model that spans multiple apps (Console, Antlers, Raptor, Getraxx, Vault) as it grows from a single-operator tool to a multi-app platform. Two structural decisions must be made before implementation begins:

  1. How permissions reach users. Three models were considered: (A) permissions attached directly to users, (B) roles attached directly to users, (C) roles carried by groups, groups assigned to users.
  2. Where the RBAC authority lives. Either each app maintains its own permission tables (federated), or a single identity service holds group/role/permission data and other apps call it (centralized).

Decision

1. Permission path: User → Groups → Roles → Permissions (option C).
No direct role or permission assignment to users is permitted. All permission grants flow through group membership.

2. RBAC authority: Centralized in Console Postgres (operator surface), with per-app tier columns for product users until a unified identity service is warranted.
Operator RBAC lives in Console Postgres. Product-user tier (free / Founders / Pro) remains a column on Raptor's users table for v1, promoted to the group model when teams/orgs ship.


Consequences

Positive: - No single user holds permissions; every grant is auditable to a group assignment event. - Adding or revoking a role for a whole team requires one group_roles write, not N user_roles writes. - SAML migration requires only that the IdP emit group names; role taxonomy stays internal. - A centralized authority means one place to query "who can do X" across all apps. - Break-glass is a group with one member, not a special user type — the model handles it without exception code.

Negative: - Centralized authority introduces a single point of failure for permission checks. Mitigation: session-scoped permission cache means Console Postgres does not need to be in the hot path for every request. - Centralized authority means Console Postgres must be highly available before Antlers can ship full RBAC. Mitigation: v1 keeps users.role tier column on Raptor until the unified path is ready. - The DAG walk adds implementation complexity vs a flat enum. Mitigation: the DAG is shallow (max 2–3 hops in the current taxonomy); a recursive CTE or in-memory walk is fast.


Alternatives Considered

A. Direct user → role assignment

Simple to implement. Fails at scale: granting a new role to 30 support agents requires 30 writes. Privilege creep is hard to audit ("who gave user X this role, and why?"). Ruled out.

B. Per-app federated RBAC tables

Each app owns its own roles and permissions. Appealing for autonomy but creates divergence: Console ops need to check permissions in Console DB, Raptor DB, Antlers DB separately. SAML migration requires synchronizing all three. Adds N call paths for a cross-app permission check (e.g., a support agent viewing Antlers data from the Console). Ruled out for v1; revisit when apps have independent deployment teams.

C. Centralized identity service (microservice)

A standalone identity service (e.g., Keycloak, Ory Keto) handles all RBAC. Correct long-term target. Premature for a team of Raxx's current size: adds infra ops overhead (another service to deploy, monitor, and keep available), requires a new auth pattern for every app, and delays shipping. The Console Postgres approach uses existing infra and migrates to a standalone service when the load and team size justify it. Revisit at the next RBAC iteration.


SAML Readiness Note

This decision does not require changing the data model when SAML is adopted. The IdP emits group names; those names are matched against the groups table. The role taxonomy is internal to Raxx and invisible to the IdP. This is the standard enterprise RBAC pattern (see AWS IAM role mapping, Okta group-push, Google Workspace directory sync).


End of ADR-0020.