ADR-0005 — OpenFGA for fine-grained authorization¶
- Status: Accepted
- Date: 2026-04-28
Context¶
Authorization in Sense is multi-tenant and relationship-shaped:
- "Can user X view tracks of org Y?" — depends on whether X is an active member of Y, and whether their role grants
can_view_tracks. - "List all detour tasks visible to user X across all their orgs" — a graph traversal.
- "User X manages role R. Inherited roles?" — recursive lookups.
In-app RBAC tables (Postgres) start simple and rot fast: a permissions table, a role_permissions table, a user_roles table — and then we need recursive CTEs for inheritance, conditional checks for org-scoped vs system-scoped permissions, and per-row authorization on list endpoints. By the time it works, it's its own framework, with worse operability than something purpose-built.
Options:
- In-app RBAC. Simple at first; ramp into a custom authz framework over time.
- Casbin / similar embedded engines. Better than rolling our own; weaker on relationship semantics and on multi-tenant model versioning.
- OpenFGA. Google Zanzibar-inspired; relationship-tuple model; first-class
BatchCheckfor list endpoints; CNCF project. - Cloud authz services (Auth0 FGA, AuthZed SpiceDB).** Comparable model; managed-only or cross-vendor.
Decision¶
Adopt OpenFGA 1.14.2, deployed in-cluster via Helm, backed by Postgres. Wrap with a thin IPermissionService in Sense API and Gen API.
Consequences¶
- Positive: Authorization model is authored as DSL (
model.fga) — readable, reviewable, version-controlled. The deployed model is pinned by SHA-256 hash in Postgres for reproducibility. - Positive:
BatchCheckmakes list-endpoint authorization a single round-trip rather than N. This was the killer feature. - Positive: Multi-tenant relationship modeling is native. Org-scoped permissions, inheritance, and time-bounded memberships all fit.
- Positive: Self-hostable. On-prem deployments work without cross-vendor calls.
- Negative: Another stateful service to operate. Postgres-backed, so it shares the platform's existing Postgres knowledge and tooling.
- Negative: Authz latency is now a network hop. Mitigated by
BatchCheckand by making the model lean (no big graphs in critical paths). - Negative: Tuple lifecycle is our problem — we have to add tuples on org join / role grant, and remove them on expiration. The
AccessExpirationJobhandles the latter.
Notes¶
The model versioning scheme (SHA-256 of model.json → row in openfga_model_versions) matters for reproducibility. Without it, "current model" is implicit and migrations between environments diverge silently.
Check is for single-row authz; BatchCheck for list endpoints. We treat per-row Check in a loop as a code smell.