Skip to content
← Blog

What OpenFGA is and how it models permissions with ReBAC

9 min read··· views
#openfga#authorization#rebac#zanzibar#permissions#architecture

If you have ever written an if (user.role === "admin" || user.id === resource.ownerId || ...) and felt every new rule making the condition a bit more fragile, this post is about the tool that solves exactly that. OpenFGA is an open authorization service, inspired by the internal system Google uses for permissions, and it is built around a simple idea: instead of storing roles, you store relationships between things, and then ask whether an operation is allowed.

The real permissions problem in an application#

Most applications start permissions in the same way. There is a role field in the users table and a couple of if statements in the code. It works perfectly until the product grows. The concept of a team appears, so now a permission also depends on which team you belong to. Then comes the guest user, which is like a normal user but with fewer powers. Then a customer wants to share a resource with someone outside their organization. And the role system, which used to be one clean line, becomes a collection of exceptions nobody fully understands anymore.

The root problem is that roles are too poor an abstraction for most modern applications. A role assumes your users fall into fixed categories, when what you really have is a changing set of relationships between users and resources. Someone is editor of a document because they created it, not because they belong to the "editor caste". OpenFGA starts by accepting that and building the system on relationships, not roles.

What OpenFGA actually is#

OpenFGA, which stands for Fine-Grained Authorization, is an open authorization service created by Auth0/Okta, released in 2022 and accepted into the CNCF in 2023. It is inspired by Zanzibar, the internal system Google published as a paper in 2019 and uses to resolve permissions in YouTube, Drive, Calendar and basically every Google product where someone has to check who can see what.

The promise of OpenFGA is that it becomes an independent piece your application asks when it needs an authorization decision. You declare your authorization model (the resource types that exist and the relationships between them), insert tuples that describe the current state (who is admin of what, which project belongs to which team), and run queries like "can this user perform this action on this resource?". OpenFGA answers true or false, in milliseconds, consistently, and the answer is the same no matter which service asks.

RBAC, ABAC and ReBAC: what changes in the model#

To understand why OpenFGA matters, it helps to place it next to the three authorization models people usually reach for.

RBAC: Role-Based Access Control#

RBAC is the default option. You have roles like "admin", "editor" or "viewer", and you assign them to users. It is easy to start with and starts to fall short as soon as hierarchy or shared resources appear between people who do not share the same role.

ABAC: Attribute-Based Access Control#

ABAC is the model where permissions are calculated from attributes (department, region, time of day, document sensitivity, whatever you need). It is very expressive and often ends up as a complex rule engine that only the person who wrote it fully understands.

ReBAC: Relationship-Based Access Control#

ReBAC, which is OpenFGA's model, starts from a different idea: what matters are the relationships between things. A user being editor of a document is a relationship. A document belonging to a folder is another. A folder having multiple parents is also a relationship. Questions are answered by walking the relationship graph, not by checking loose attributes or matching against roles.

The important bit is that most real rules in large applications are expressed more naturally as relationships than as roles. "The editor of a document can share it" is a relationship. "The team admin can edit any project in the team" is a relationship with propagation. ReBAC models this without gymnastics, because it is exactly the language you already use in your head before translating permissions into code.

ModelQuestion it asksWhere the logic livesFits well when...Starts to break when...
RBACWhat role does this user have?In global or scoped rolesThere are a few stable profilesThe permission depends on the specific resource
ABACWhich attributes satisfy this rule?In policies and conditionsContext, region, time, flags or metadata matterThe rules start to look like their own language
ReBACWhich relationship connects the user to the resource?In the relationship graphThere is hierarchy, ownership, teams or shared resourcesThe problem is purely contextual and not relational

The model in code#

To show what an OpenFGA model looks like, I will use the most typical SaaS hierarchy: an organization (tenant) contains teams (group), and resources (resource) live inside each team. Actors are of type user.

authorization-model.fga
model
  schema 1.1
 
type user
 
type tenant
  relations
    define admin: [user]
    define member: [user] or admin
 
type group
  relations
    define tenant: [tenant]
    define lead: [user] or admin from tenant
    define staff: [user] or lead
 
type resource
  relations
    define group: [group]
    define editor: [user] or lead from group
    define viewer: [user] or staff from group or editor

There are three hierarchical resource types (tenant, group, resource) and one subject type (user). Each resource declares its relationships, and each relationship can be direct (inside brackets, assignable with a tuple) or computed (the part after an or, calculated from another relationship without you assigning it manually).

The important line is or admin from tenant. You are telling OpenFGA that the condition "lead of a team" is true not only when someone is explicitly a lead, but also when they are admin of the tenant the group belongs to. Permission propagation downward is declared once, in the model, and applies from then on.

Tuples: the state of your platform#

The model is the schema. Tuples are the data. Each tuple is a triplet (user, relation, object) and you write them as things happen in your system. For this example, three tuples are enough:

initial-state.fga
user:01      admin    tenant:01
tenant:01    tenant   group:01
group:01     group    resource:01

What you are storing is that user:01 is admin of tenant:01, that group:01 belongs to that tenant, and that resource:01 is inside that group. You never said user:01 can edit resource:01, and you do not need to. The model derives it by itself, because or admin from tenant chains the path from tenant to group, and or lead from group chains the path from group to resource.

The key question: Check#

The most common OpenFGA operation is Check. You pass it the triplet of the question and receive the answer:

check.ts
const { allowed } = await fga.check({
  user: "user:01",
  relation: "editor",
  object: "resource:01",
});
 
// allowed === true

Internally, OpenFGA is not reading a flat list of permissions. It tries to prove the requested relationship using the tuples in the store and, if it does not find a direct assignment, follows the computed branches declared by the model.

  1. It asks whether user:01 editor resource:01 exists. It does not, so it moves to the computed branches of the model.

  2. editor = [user] or lead from group, so it needs to know whether user:01 is lead of the group resource:01 belongs to.

  3. It finds the tuple group:01 group resource:01, so the question becomes user:01 lead group:01.

  4. lead = [user] or admin from tenant, so it needs to know whether user:01 is admin of the tenant group:01 belongs to.

  5. It finds the tuple tenant:01 tenant group:01, so the question becomes user:01 admin tenant:01.

  6. user:01 admin tenant:01 exists, so the path is valid and the final result is allowed = true.

# store · preloaded tuples
tenant:01
├── admin: user:01
├── group:01
│   ├── lead: user:02
│   ├── staff: user:05, user:06
│   └── resource:01
│       └── editor: user:04
└── group:02
    ├── lead: user:03
    ├── staff: user:07
    └── resource:02

# no tuples
user:08

# change user, relation or object and press run

$fga check--user--relation--object

Each check shows the verdict and the proof: tuples read, computed rules and conclusion. Try user:05 viewer resource:01 to see a permission derived through staff from group, user:01 editor resource:02 to see the admin -> lead -> editor chain, or any relation for user:08 to see a case with no path in the graph.

OpenFGA exposes a few other operations worth knowing. ListObjects returns every resource a user has a certain permission on, which is the right way to filter lists without checking items one by one. ListUsers returns every user with permission on a resource, which is exactly what you need to render an access management screen. And Expand shows the relationship tree that justifies a permission, which is useful when something does not return what you expected and you need to debug it.

Where OpenFGA fits in your architecture#

OpenFGA runs as a separate service, usually deployed inside your network, with its own database (Postgres or MySQL). Your application talks to it over gRPC or HTTP depending on the SDK you use. The usual pattern is that the gateway or API service receives an already-authenticated request, extracts the user identity, calls Check with the operation the user wants to perform, and only continues with business logic if the answer is positive.

Authentication remains the responsibility of another piece, whether that is your own identity service, an external IdP or the usual JWT. OpenFGA only answers "can this user do this on this thing?", not "who is this user?". That separation of responsibilities is deliberate and useful, because each piece has one job.

When OpenFGA makes sense#

It makes sense when your permissions model has hierarchy or shared resources. If your application needs to propagate permissions downward, like when a team admin can see every project in the team, or sideways, like when a document is shared with an individual user or with a whole team, ReBAC is the natural model and OpenFGA is the most battle-tested open source implementation of that model.

It makes sense when you work with multiple services that share identity and need to agree on who can do what. Centralizing authorization in OpenFGA prevents every service from reimplementing the same logic with tiny deviations that later become bugs you spend days tracking down.

It makes sense when you need auditability. Every tuple change is recorded, which gives you a history of who had access to what at each point in time without having to build that audit system yourself.

When it is not worth it#

It is not worth it for a small application with a couple of fixed roles and a three-person team. A role field in the users table and two if statements in the code is the right answer up to a certain size, and anything more sophisticated is over-engineering.

It does not fit well with purely contextual rules like "block access outside working hours" or "allow only from these IPs". Contextual tuples exist to patch cases like that, but if your logic is mostly ABAC you will be fighting the tool. In those scenarios it usually makes more sense to combine OpenFGA for the relational part with a separate policy layer (something like OPA) for contextual rules.

Summary#

OpenFGA changes the question. Instead of asking every service "what permissions does this user have?", you ask a single service "can this user do this?". It looks like a small change on the surface, but underneath it reorganizes how you think about permissions, decouples them from each service's code, and puts them in one place that is queryable and auditable. If you build applications with hierarchy or shared resources, it is worth at least modeling your system in the OpenFGA playground and seeing how much code disappears.

If you are interested in how this fits into a real case, in another post I explain why we put OpenFGA in from the first commit on a platform with many microservices, and which concrete problems that decision saved us from.