Skip to content

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:

  1. In-app RBAC. Simple at first; ramp into a custom authz framework over time.
  2. Casbin / similar embedded engines. Better than rolling our own; weaker on relationship semantics and on multi-tenant model versioning.
  3. OpenFGA. Google Zanzibar-inspired; relationship-tuple model; first-class BatchCheck for list endpoints; CNCF project.
  4. 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: BatchCheck makes 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 BatchCheck and 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 AccessExpirationJob handles 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.