Another story, another article. A client asked me recently:
π£οΈ Can we add temporary permissions for a group of users assigned to a maintenance task, while it's ongoing?
It should be simple enough, right? Yes, until I examined the authorization code behind the API and found a 500-line function checking user roles and groups, time windows, resource ownership, and various business rules. πΆβπ«οΈ
Unlike authentication (who is accessing the system), where we have OIDC, JWT, and other established standards and patterns, authorization (what they can do) often forces us into custom implementations.
π«·You might argue that OAuth 2.0 cover authorization, but they focus on third-party access, not complex and dynamic authorization patterns.
The OWASP Top 10 API Security Risks lists Broken Object Level Authorization as the #1 risk, showing us how common it is to expose sensitive data due to poor authorization checks.
Is it far-fetched to think that the complexity of authorization logic contributes to this risk?
Each new policy adds another conditional branch, another database join, another custom role, another edge case that breaks during the next feature request. The authorization flow becomes a spaghetti bowl and even experienced developers hesitate before touching it.

Traditional approaches quickly hit walls:
- RBAC works until you need "sometimes" permissions
- ABAC offers flexibility but becomes a rule engine nightmare
- Database queries slow to a crawl as your permission matrix grows
What if there is a better way? A way that lets you express complex relationships without messy code or performance hits? π€
My exploration for a better paradigm started with Ory Keto:

Integrate Ory in a NestJS application
Edouard Maleix γ» May 16 '24
It introduced me to Google's Zanzibar paper and the concept of Relation-Based Access Control (ReBAC).
This time, I needed more flexibility to introduce contextual relationships. That "simple" feature request led me to OpenFGA β a richer implementation of Zanzibar's principles that extends ReBAC with powerful features like contextual-based conditions, attribute-based access, and a simple query language.
And since you might be familiar with this story, I'll share with you my learning journey, starting from the concepts and terminology, through practical examples and considerations, to real-world usage of OpenFGA.
- β The Authorization Problem
- π Why OpenFGA? β You are here
- β¬ ReBAC and OpenFGA concepts
- β¬ OpenFGA in Action
- β¬ Testing permissions with OpenFGA CLI
- β¬ Adoption Challenges and Strategies
Why OpenFGA [ββββββββ]
Before I grab your attention and your brain π§ with the ReBAC concepts and how OpenFGA implements them, let me explain why I chose OpenFGA over other solutions.
It Matches How You Think
Expressive Relationships
Cat owners own cats. Sitters sit cats. Admins administrate. The authorization model mirrors reality instead of forcing you into artificial role hierarchies. Demo
Time Works Automatically
No more "grant permission at 9 AM, revoke at 5 PM" cron jobs. Time-based access happens naturally through conditions.
Grant permissions only when conditions are metβlike during scheduled hours.
Demo
π€ Yes! My client is going to love this.
Status Drives Decisions
Your app's workflow probably includes some entities' states (e.g., pending, active, completed). OpenFGA uses these attributes directly for permissions instead of requiring separate access control flags. Demo
Queries, Not Just Checks
Traditional systems answer "Can Alice do X?" OpenFGA also answers "What can Alice do?" and "Who can do X?" This opens opportunities for features like smart dashboards and permission audits.
Demo


Scale Like Google
Google's Zanzibar (which inspired OpenFGA) handles billions of authorization checks daily. Your application(s) probably won't hit those numbers, but it's nice to know you won't hit a wall due to a poorly performing authorization system.
βοΈ In my tests, OpenFGA performed slightly better than custom database lookups for complex relationships (both based on PostgreSQL).
Great Documentation and CLI Tools
I can't deny it, OpenFGA has a steep learning curve, but its documentation is complete and well-structured. It covers everything from basic concepts to advanced usage patterns until deployment strategies.
The CLI tools make it easy to manage your authorization model and test your policies.
Business Backing
OpenFGA is open-source and Okta is funding it, this ensures a long-term viability and support. On one side, you can always deploy it on your own infrastructure, and the community is growing rapidly. On the other side, Okta has strong competitors like OSO, Ory Keto or AWS Cedar, so they have a vested interest in making OpenFGA a successful product extending what Auth0 has to offer.
Deployment Flexibility
OpenFGA can be deployed in various ways, depending on your needs:
- Self-hosted: Use Docker Compose to get started locally, Kubernetes for full control and Terraform to orchestrate your infrastructure.
- Managed service: Use Auth0's FGA offering for a hassle-free experience.
Observability and Debugging
You can configure:
- OpenTelemetry for traces collection on the client and the server side.
- Prometheus for metrics collection.
This makes it easier to monitor your authorization system with open standards and integrate with your existing observability stack.
Simpler Code To Maintain
To illustrate this, I'm using Typescript to check if a user can update a cat sitting arrangement with both approaches: a plain database lookup and an OpenFGA check.
Database Lookup
async function isSystemAdmin(userId: string): Promise<boolean> {
const user = await userRepository.findById(userId);
if (!user) return false;
return user.role === 'admin';
}
async function checkCatSittingUpdatePermission(
userId: string,
sittingId: string
): Promise<boolean> {
const sitting = await catSittingRepository.findById(sittingId);
if (!sitting) return false;
const cat = await catRepository.findById(sitting.catId);
if (!cat) return false;
const isOwner = cat.ownerId === userId;
const isSitter = sitting.sitterId === userId;
const isAdmin = () => await isSystemAdmin(userId);
const isPending = () =>
sitting.status === 'requested' && new Date(sitting.startTime) > new Date();
return isOwner || (isSitter && isPending()) || isAdmin();
}
OpenFGA Check
async function checkCatSittingUpdatePermission(
userId: string,
catSittingId: string
): Promise<boolean> {
const openfgaClient = new OpenFgaApi({
apiUrl: process.env.OPENFGA_API_URL,
});
const request: CheckRequest = {
tuple_key: {
user: `user:${userId}`,
relation: 'can_update',
object: `cat_sitting:${catSittingId}`,
},
context: {
current_time: new Date().toISOString(),
},
};
const { allowed } = await openfgaClient.check(
process.env.FGA_STORE_ID,
request
);
return !!allowed;
}
Does it need a lot of explanation? The OpenFGA version is objectively cleaner, more maintainable, and scales better as your authorization logic grows.
ReBAC and OpenFGA concepts [βββββββ]
I'll walk you through ReBAC using PurrfectSitter Β©, a cat sitting app where owners find sitters. Real problems, real solutions.
As trivial as it sounds, this example shows:
- Role-based access control (RBAC) for admins
- Attribute-based access control (status-driven permissions)
- Time-based access control
- Resource ownership and management
Three Building Blocks
ReBAC builds authorization from three simple pieces:
Types: Entities in Your App
type user
type system
type cat
type cat_sitting
type review
These map to your app's core entities:
-
user
: π€ People using your app -
system
: π’ Admin access controls -
cat
: π± Furry clients needing care -
cat_sitting
: π A sitting arrangement -
review
: π Post-sitting feedback
Each type will declare relationships with other types - in the type definition.
βΉοΈ In Ory Keto, these are called namespaces.
Objects: Instances of Types
In OpenFGA, an object is an instance of a type. For example:
-
user:bob
: A specific user named Bob -
cat:romeo
: A specific cat named Romeo -
system:development
: The development environment -
cat_sitting:1
: The first cat sitting arrangement -
review:1
: The first review
Objects are the concrete entities your users interact with.
Users: The Actors
A user is an entity that is related to objects in your system. In our app, users can be:
- People (like Bob)
- Systems (like the PurrfectSitter development environment)
- Cats (like Romeo)
βΉοΈ In Ory Keto, these are called subjects. I believe subject is less ambiguous than user, but OpenFGA uses user, so we will too.
Relations: How Things Connect
A relation defines how users interact with objects. For example:
-
user:bob owner cat:romeo
: Bob is the owner of Romeo -
user:anne sitter cat_sitting:1
: Anne is the sitter for the first cat sitting arrangement
Each relation (e.g., admin
) evaluation logic is defined in the relation definition (e.g., [user]
).
type system
relations
define admin: [user]
type cat
relations
define owner: [user]
define admin: admin from system
define can_manage: owner or admin
define system: [system]
type cat_sitting
relations
define active_sitter: [cat_sitting#sitter with is_active_timeslot]
define can_post_updates: owner or active_sitter
define can_review: [cat#owner with is_cat_sitting_completed]
define cat: [cat]
define owner: owner from cat
define sitter: [user]
βΌοΈ For the sake of this example, we will assume that cats are owned by humans. We all know that, in reality, cats own us, not the other way around.
OpenFGA computes relationships in several ways:
-
Direct:
-
system.admin
β A user can be an admin of the system -
cat.owner
β A user can be a cat owner -
cat.system
β A system can be assigned to a cat -
cat_sitting.sitter
β A user can be a sitter for a cat_sitting
-
-
Implied:
-
cat.admin: admin from system
β An admin is a user from the system -
cat_sitting.owner: owner from cat
β The cat_sitting owner is the cat owner
-
-
Union:
-
cat.can_manage
β either the cat owner or an admin from the system can manage the cat -
cat_sitting.can_post_updates
β either the cat_sitting owner or an active_sitter can post updates
-
-
Conditional:
-
cat_sitting.active_sitter
β Conditional relation between user and cat_sitting based on the outcome of theis_active_timeslot
condition -
cat_sitting.can_review
β Conditional relation between user and cat_sitting based on the outcome of theis_cat_sitting_completed
condition
-
There are even more ways to express relationships, such as exclusion, intersection and nesting, you can find the complete configuration language reference in the OpenFGA documentation.
The Complete Authorization Model
The ensemble of types and relations definitions forms the authorization model.
Here, the PurrfectSitter's authorization model in OpenFGA's configuration language (Domain-Specific Language for the purist), defines how users interact with cats, cat sittings, and reviews.
model
schema 1.1
type user
type system
relations
define admin: [user]
type cat
relations
define admin: admin from system
define can_manage: owner or admin
define owner: [user]
define system: [system]
type cat_sitting
relations
define admin: admin from system
define active_sitter: [cat_sitting#sitter with is_active_timeslot]
define pending_sitter: [cat_sitting#sitter with is_pending_timeslot]
define can_post_updates: owner or active_sitter
define can_delete: admin or owner or pending_sitter
define can_view: admin or owner or sitter
define can_update: admin or owner or pending_sitter
define can_review: [cat#owner with is_cat_sitting_completed]
define cat: [cat]
define owner: owner from cat
define sitter: [user]
define system: [system]
type review
relations
define admin: admin from system
define author: owner from cat_sitting
define can_delete: admin or author
define can_edit: admin or author
define can_view: [user, user:*]
define cat: cat from cat_sitting
define cat_sitting: [cat_sitting]
define subject: sitter from cat_sitting
define system: [system]
condition is_active_timeslot(current_time: timestamp, end_time: timestamp, start_time: timestamp) {
current_time >= start_time && current_time <= end_time
}
condition is_pending_timeslot(current_time: timestamp, start_time: timestamp) {
current_time < start_time
}
condition is_cat_sitting_completed(cat_sitting_attributes: map<string>, completed_statuses: list<string>) {
cat_sitting_attributes["status"] in completed_statuses
}
Notice how readable, yet compact, this is β no complex SQL joins or nested conditions. The model captures business logic naturally.
β Checkpoint: Can You Answer These?
Before moving on, make sure you can answer:
- What's the difference between a user and an object?
- How do relations differ from roles?
- When would you use indirect relationships?
Answers
OpenFGA in Action [βββββββ]
Let's test our model with real scenarios. I use the OpenFGA CLI to initialize the authorization model, create relation tuples, check the permissions and run some querie but you can use any other client SDK.
Save some time, create a GitHub codespace
It will provide you a ready-to-use environment with all dependencies installed and external services running, so you can focus on running the examples in this article.
Setup OpenFGA
1. Creating a Store and a Model
First, create a store:
Then create the authorization model in the new store:
β οΈ If you are using Codespaces, specify the API path with
- the flag
--api-url http://openfga:8080
- the environment variable
FGA_API_URL=http://openfga:8080
2. Creating Basic Relationships
Bob owns Romeo, Anne sits for him. Simple.
3. Admin Powers
Jenny becomes a system admin who can manage any cat β traditional RBAC within ReBAC.
4. Time Magic
Anne's permissions activate and deactivate automatically based on time. No cron jobs, no cleanup code β the authorization system handles it.
5. Status-Driven Access
Reviews only make sense after sitting ends. OpenFGA enforces this business rule automatically, ABAC style.
6. Creating and Checking Review Permissions
Create a review and check who can edit or delete it. OpenFGA's query language shines here, allowing you to check permissions and also list objects a user can interact with.
7. Making the Review Public
Control visibility using wildcards.
Explore Relationships with OpenFGA Playground
You can visualize the relations graph and run queries in the OpenFGA's Playground.
I find it a great way to discover and understand relationships in your model and test queries interactively.
π‘ If you are using Codespaces, just open
http://localhost:8082/playground
in your browser.
Testing permissions with OpenFGA CLI [βββββββ]
Another one of OpenFGA's strengths, is its built-in testing capabilities. The CLI provides a declarative way to test authorization models without writing application code.
Declarative Testing with YAML
Define tests in YAML and run with a single command:
fga model test --tests store.fga.yml
...and forget about all the commands above π. The store.fga.yml
file contains everything you need to create the model and tuples, and run the tests before writing application code!
Let's look at the store.fga.yml
file that tests our PurrfectSitter model:
The authorization model
This is the model we defined earlier, but in YAML format for the OpenFGA CLI:
model: |
model
schema 1.1
# Our full model definition goes here...
βΉοΈ The model section is the same as the one we defined earlier, but in YAML format for the OpenFGA CLI.
The tuples
This section defines the relationships (tuples) in our model. Each tuple represents a relationship between a user and an object, along with the relation type.
model:
# ...
tuples:
- user: user:jenny
relation: admin
object: system:development
- user: user:bob
relation: owner
object: cat:romeo
- user: system:development
relation: system
object: cat:romeo
- user: cat:romeo
relation: cat
object: cat_sitting:1
- user: user:anne
relation: sitter
object: cat_sitting:1
- user: cat_sitting:1#sitter
relation: active_sitter
object: cat_sitting:1
condition:
name: is_active_timeslot
context:
start_time: '2023-01-01T00:00:00Z'
end_time: '2023-01-02T00:00:00Z'
- user: cat:romeo#owner
relation: can_review
object: cat_sitting:1
condition:
name: is_cat_sitting_completed
context:
completed_statuses: ['completed']
- user: system:development
relation: system
object: review:1
- user: cat_sitting:1
relation: cat_sitting
object: review:1
- user: user:*
relation: can_view
object: review:1
The tests
This section defines the tests that will be run against the model and tuples. Each test checks specific permissions or relationships.
The example demonstrates several test types:
- Basic permission checks: Simple assertions about relationships
- Contextual checks: Testing time-based permissions
- Attribute-based checks: Testing permissions depending on object attributes
- List objects: Finding objects a user has relationships with
- List users: Finding users with relationships to an object
model:
# ...
tuples:
# ...
tests:
- name: Test basic relations
check:
- user: user:anne
object: cat_sitting:1
assertions:
sitter: true
- user: user:bob
object: cat_sitting:1
assertions:
owner: true
- name: Test role access
check:
- user: user:jenny
object: cat:romeo
assertions:
can_manage: true
- user: user:bob
object: cat:romeo
assertions:
can_manage: true
- user: user:anne
object: cat:romeo
assertions:
can_manage: false
- name: Test temporal access
check:
- user: user:anne
object: cat_sitting:1
context:
current_time: '2023-01-01T00:10:00Z'
assertions:
active_sitter: true
- user: user:anne
object: cat_sitting:1
context:
current_time: '2023-01-04T00:00:00Z'
assertions:
active_sitter: false
- name: Test attribute access
check:
- user: user:bob
object: cat_sitting:1
context:
cat_sitting_attributes:
status: 'completed'
assertions:
can_review: true
- user: user:bob
object: cat_sitting:1
context:
cat_sitting_attributes:
status: 'in_progress'
assertions:
can_review: false
- name: Test the cat sitting that anne is sitting
list_objects:
- user: user:anne
type: cat_sitting
context:
current_time: '2023-01-01T00:00:01Z'
assertions:
active_sitter:
- cat_sitting:1
sitter:
- cat_sitting:1
- name: Test the review that bob can edit
list_objects:
- user: user:bob
type: review
assertions:
can_edit:
- review:1
- name: Test that reviews are public
list_users:
- object: review:1
user_filter:
- type: user
assertions:
can_view:
users:
- user:*
π You can find
store.fga.yml
in the demo repository.
Testing During Adoption
These testing capabilities help when adopting OpenFGA:
- Validate models against business rules
- Verify permissions match the old system during migration
- Compare results with your existing system in shadow mode
- Prevent regressions with CI pipeline tests
Including tests in your workflow reduces authorization errors and builds confidence in your implementation.
β Checkpoint II: Can You Answer These?
Have you read carefully the previous sections? If so, you should be able to answer:
- Can you list objects a user has relationships with?
- Can you list users with relationships to an object?
- Can you make an object public to all users?
List Objects: Yes, you can use the list-objects command to find objects a user has relationships with, like finding all cat sittings where a user is an active sitter. List Users: Yes, if you have a relationship that connects users to objects, you can use the list-users command to find users with relationships to an object, like finding all users who can view a specific review. Public Objects: Make an object public by adding a relation that allows all users to access it, like granting Answers
user:*
permission on the object as I showed you in this example
Adoption Challenges and Strategies [βββββββ]
As good as this tool is, adopting OpenFGA in existing systems presents challenges.
Mental Model Shift
ReBAC requires a paradigm shift for developers:
- Mental model adjustment: Developers familiar with RBAC or ABAC need time to think in relationships.
- Training investment: Workshops and examples help teams translate existing rules into relationship models.
Data Synchronization
This is probably the most challenging aspect of adopting OpenFGA, especially if you have an existing database with complex permissions.
- Dual writes: Applications must write to both their database and OpenFGA.
-
Synchronization strategies:
- Event-driven synchronization through message queues
- Centralized hooks for database operations
- Transactional outbox pattern for consistency
- Background jobs for existing data
Read this excellent article π about dual writes in distributed systems. It will surely help you understand strategies for synchronizing data between your applications' DB and OpenFGA.
Progressive Adoption
It's going to be hard (and unwise) to convince your team to rewrite the entire authorization logic in OpenFGA, big-bang refactoring style. Instead, consider a progressive adoption strategy:
1. Start with Coarse-Grained Permissions
Begin with your existing structure:
- Replicate your current RBAC model
- Add organization-level permissions
- Gradually introduce finer-grained controls
2. Shadow Mode Implementation
Before switching fully:
- Run existing authorization alongside OpenFGA
- Compare results to identify discrepancies
- Build confidence before making the switch
3. Use Contextual Tuples for Hybrid Implementations
Reduce synchronization burden:
- Send data as contextual tuples initially
- Gradually move to persistent relationship tuples
- Use contextual tuples for frequently changing data
βΉοΈ Read more about this technique in the OpenFGA documentation.
Managing Organizational Adoption
For large organizations:
- Start with a single application where OpenFGA delivers immediate value
- Use modular models for independent team control
- Leverage access control for team-specific credentials
Your Next Move [βββββββ]
Complex policies doesn't have to mean complex code. OpenFGA's ReBAC model simplifies permissions into relationships, making your authorization logic more maintainable and scalable.
Ready to get started? Here's your roadmap:
- Read in details the PurrfectSitter's authorization model
- Draw inspiration from the Purrfect Sitter Fastify API
- Adapt it to your domain
- Watch complex permission logic become simple relationship definitions.
- Get in touch with me for some deep-dive into performance characteristics, monitoring, and production deployment patterns π
- If you want to show your appreciation, give those repositories a βοΈ on GitHub. π
getlarge
/
purrfect-sitter
A cat sitting management application to showcase OpenFGA
Purrfect Sitter
A cat sitting management application with dual authorization strategies.
Architecture
Core Application
- Fastify-based API
- User authentication with Ory Kratos
- Core models: User, Cat, CatSitting, Review
- TypeBox for JSON Schema validation
- Drizzle ORM for database access
- OpenFGA for fine-grained authorization
NX Workspace Structure
apps/
purrfect-sitter/ # Main Fastify application
purrfect-sitter-e2e/ # End-to-end tests
libs/ # Domain-specific libraries
database/ # Database schema and repositories
models/ # DTOs and validation schemas
auth/ # Authentication and authorization
cats/ # Cats domain business logic
cat-sittings/ # Cat sittings domain business logic
reviews/ # Reviews domain business logic
Authorization Strategies
This application implements two authorization strategies that can be toggled via environment variables:
-
Database Lookups (
AUTH_STRATEGY=db
)- Traditional database queries to check permissions
- Uses JOINs and WHERE clauses for relationship checks
- Direct SQL conditions for time-based rules
-
OpenFGA (
AUTH_STRATEGY=openfga
)- Relationship-based authorization using OpenFGA
- Declarative authorization model (authorization-model.fga)
- Uses relationship tuples forβ¦
openfga
/
openfga
A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar
OpenFGA
A high-performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar.
OpenFGA is designed to make it easy for developers to model their application permissions and add and integrate fine-grained authorization into their applications.
It allows in-memory data storage for quick development, as well as pluggable database modules. It currently supports PostgreSQL 14, MySQL 8 and SQLite (currently in beta).
It offers an HTTP API and a gRPC API. It has SDKs for Java, Node.js/JavaScript, GoLang, Python and .NET. Look in our Community section for third-party SDKs and tools. It can also be used as a library.
Getting Started
The following section aims to help you get started quickly. Please look at our official documentation for in-depth information.
Setup and Installation
βΉοΈ The following sections setup an OpenFGA server using the default configuration values. These are for rapid developmentβ¦
Your future self will thank you for choosing relationships over nested IF statements.
Top comments (2)
Great and well structured post! I'm curious to try it out π
Great post, i have implemented this one spring boot and Rbac is really an important concept