OpenFGA ReBAC: Fine-Grained Multi-Tenant Authorization

Photo by Alina Grubnyak on Unsplash
OpenFGA is an open-source, Cloud Native Computing Foundation authorization engine for relationship-based access control (ReBAC). It is inspired by Google's Zanzibar authorization system, and its JSON model syntax closely follows the original Zanzibar paper. You store relationships as tuples and query them with a Check API.
RBAC assigns permissions through static roles like editor or admin, which breaks down with hierarchy, sharing, and multi-tenancy. ReBAC makes access conditional on relationships between users and objects, and between objects themselves. OpenFGA calls ReBAC a superset of RBAC that also covers attribute-based scenarios when attributes are expressed as relationships.
A relationship tuple is the unit of authorization data: a user, a relation, and an object, with an optional condition written in Google's CEL language. For example, user:anne is editor of document:q3-roadmap. The user can be a specific subject, another object, a userset like a group's members, or a public wildcard.
A store organizes authorization data and its data cannot be shared across stores. Most multi-tenant apps use one shared store with a tenant or organization type in the model, which keeps cross-tenant sharing possible. Use a store per tenant only when you need hard data isolation, per-tenant residency, or separate scaling.
Choose OpenFGA when access is dominated by relationships you must traverse, such as folders, teams, and shared documents. Choose Cerbos, a stateless policy decision point that evaluates YAML policies over attributes, when decisions hinge on request-time attributes like time, resource status, or IP. Cerbos does not store relationship data itself.

Photo by Alina Grubnyak on Unsplash
Key Takeaway
OpenFGA is an open-source, Google Zanzibar-inspired engine for relationship-based access control. Instead of assigning static roles, you store relationship tuples like anne is editor of document q3-roadmap, define an authorization model of types and relations, and ask the Check API whether a user has a relation on one specific object.
Every multi-tenant product I have worked on starts with role-based access control because it is simple: give a user the editor role, and editors can edit. That model holds right up until a customer asks to share a single document with an outside collaborator, or nest projects inside folders inside workspaces, or grant read access to everything a manager's reports own. Suddenly one flat role per user cannot express who can touch what.
The pattern that answers those questions is relationship-based access control, or ReBAC, and the tool I reach for is OpenFGA, a Cloud Native Computing Foundation project inspired by Google's Zanzibar authorization system. This post walks through where RBAC breaks, how ReBAC reframes the problem as relationships, and how OpenFGA implements it with an authorization model, relationship tuples, and a Check API.
RBAC assigns permissions to users based on roles like editor or admin. OpenFGA's own documentation is blunt about the limits: RBAC fits flat, single-tenant access models but breaks down with hierarchy, sharing, or multi-tenancy. Three failure modes show up again and again once a real product grows.
Each of these is really a question about a relationship between a user and a specific object, or between two objects. RBAC has no vocabulary for relationships, so every relationship gets flattened into another role. That is the trap.
ReBAC makes access rules conditional on relationships between users and objects, and between objects themselves. Fine-grained authorization then means deciding access at the level of the individual resource and action: Alice can edit document-42, not merely Alice is an editor. That single shift is what lets you answer the sharing, nesting, and per-resource questions without inventing new roles.
OpenFGA describes ReBAC as a superset of RBAC that also natively covers attribute-based scenarios when attributes are expressed as relationships. In practice you rarely throw roles away; you express a role as just one kind of relationship among many, so an admin relation and a shared-with relation live in the same model and are evaluated the same way.
A useful mental test: if a permission question contains the words this specific or shared with or inside, it is a relationship question, not a role question. Those are exactly the cases where ReBAC pays for itself and where a role table would otherwise sprawl.
An OpenFGA authorization model combines one or more type definitions to describe the permission model of a system. You can write it as JSON that closely follows the original Zanzibar paper syntax, or in a friendlier DSL. Here is a small model in the DSL for a document store with organizations, folders, and inherited permissions.
model
schema 1.1
type user
type organization
relations
define member: [user]
type folder
relations
define owner: [user, organization#member]
define viewer: [user, organization#member]
type document
relations
define parent: [folder]
define owner: [user, organization#member]
define editor: [user, organization#member]
define viewer: [user, organization#member]
define can_view: viewer or editor or owner or viewer from parent
define can_edit: editor or owner
define can_share: owner or owner from parentRead define can_view as viewer or editor or owner or viewer from parent. The viewer from parent clause is the inheritance that a flat role cannot express: anyone who is a viewer of the document's parent folder automatically can_view the document, with no extra tuples per document. The organization#member notation lets a relation be granted to every member of an organization at once.
The model is the schema; relationship tuples are the data. A tuple is a user, a relation, and an object, with an optional condition expressed in Google's CEL language. The user can be a specific subject, another object, a userset like a group's members, or the public wildcard. You ask questions with the Check API, which answers whether a user has a given relation on a given object.
# 1. A relationship tuple: user -> relation -> object
{
"user": "user:anne",
"relation": "editor",
"object": "document:q3-roadmap"
}
# 2. A tuple linking a document to its parent folder
{
"user": "folder:planning",
"relation": "parent",
"object": "document:q3-roadmap"
}
# 3. A Check request: does anne have can_view on the doc?
POST /stores/{store_id}/check
{
"tuple_key": {
"user": "user:anne",
"relation": "can_view",
"object": "document:q3-roadmap"
}
}
# Response
{ "allowed": true }Check returns a plain allowed true or false, and it evaluates inheritance for you: because a parent tuple links the document to folder planning, a viewer of that folder passes the can_view check without any document-level tuple. When you need the inverse questions, OpenFGA offers ListObjects to find every object a user can access, ListUsers, Expand, Read, and BatchCheck.
Check answers does user X have relation Y on object Z, and is deliberately not built to answer who has access to Z or what can X access. Reaching for Check in a loop to build a list is the classic performance mistake; use ListObjects or ListUsers for those, and BatchCheck when you must evaluate many pairs at once.
A store is the OpenFGA entity that organizes authorization data, holding your model versions and tuples. The rule that shapes multi-tenant design is that store data cannot be shared across stores, so related authorization data should live together in a single store. That single fact frames the whole trade-off.
For most multi-tenant SaaS the simpler and more common design is one shared store with a tenant or organization type in the model, scoping every object to its tenant. It keeps cross-tenant sharing possible and operations cheap. A store per tenant is the heavier option you reach for only when you need hard data isolation, per-tenant retention or residency, or separate scaling, accepting that you can no longer answer a single Check across two tenants.
OpenFGA is not the only way to do fine-grained authorization. Cerbos takes the policy-based, attribute-based route: a stateless policy decision point that evaluates YAML policies combining roles with conditional attribute logic. The key architectural difference is where the data lives. OpenFGA stores your relationship graph and traverses it; Cerbos does not store relationship data itself and instead evaluates against attributes you supply at request time from your own identity provider and databases.
| Dimension | OpenFGA (ReBAC) | Cerbos (policy-based ABAC) |
|---|---|---|
| Core model | Relationship graph of tuples | YAML policies over attributes |
| Stores data | Yes, tuples plus the model | No, stateless PDP by design |
| Natural strength | Nesting, sharing, group inheritance | Contextual rules on request attributes |
| Answers who or what queries | Yes, via ListObjects and ListUsers | No, it returns a per-request decision |
| Lineage | Google Zanzibar inspired | Policy-as-code decision point |
Neither is strictly better. Choose OpenFGA when access is dominated by relationships you must traverse, like folders, teams, and shared documents. Choose a policy engine like Cerbos when decisions hinge on request-time attributes such as time of day, resource status, or IP, which are awkward to encode as stored relationships. Real systems sometimes run both.
If you are hitting role explosion, start by writing down the three or four questions your role check keeps failing to answer, then model just those as relations in a single OpenFGA store. Prove one inherited permission end to end with a Check, and you will have a schema that grows with sharing and nesting instead of fighting them.