At a glance
Why OpenFGA
Openlane builds the kind of platform whose customers will themselves be audited. Authorization had to be defensible end-to-end — every read, every write, every export — and could not live in aif user.role == "admin" switch sprinkled across resolvers. The team picked OpenFGA so authorization decisions were centralized, modeled as relationships, and could evolve without code changes in every service.
A second motivation was packaging: Openlane sells modules, and the same engine that grants access to a record can grant access to a feature. OpenFGA is the source of truth for both.
Architecture
- Authorization as ent middleware. Openlane uses ent hooks to write tuples on every mutation and interceptors plus policies to evaluate every query. Resolvers do not call the OpenFGA SDK directly; the data-access layer does.
- Object-owned mixin. A reusable ent mixin attaches
ownerand parent relations to any record type, so cascading permissions (“an editor of the parent program can edit each control”) are declared once and applied uniformly. - Overfetch + BatchCheck instead of ListObjects. An early implementation used ListObjects and saw ~8-second worst-case latency for large result sets. The team switched to overfetching candidates from Postgres (capped at 100 per page, up to 1,000 overfetched) and running BatchCheck to filter in a single round trip. Total counts are computed via a separate query that short-circuits when the user is an admin.
- In-house wrapper packages. Three small Go packages —
FGAX(typed helpers around the OpenFGA SDK),entfga(ent hooks that write tuples), andaccess-map(declarative relation registration) — keep callers honest and make adding a new entity type a few lines of mixin configuration. - OpenFGA-as-feature-flags. Module entitlements (“does this tenant have the policy module?”) live in the same OpenFGA store as record-level permissions, so a check that returns
falsebecause the user is not an editor and a check that returnsfalsebecause the tenant did not buy the module use the same call site.
Outcomes
- Authorization can’t be forgotten. Hooks and interceptors mean a new entity type inherits authorization automatically.
- List-style endpoints went from ~8 s worst case to comfortably under a second at expected page sizes, without changing the public API.
- One store, two jobs. Record permissions and feature entitlements live in OpenFGA, so packaging changes don’t require a second policy system.
- Auditable end-to-end. Tuple writes happen in the same transaction boundary as the underlying data mutation, so the access graph and the data it protects don’t drift.