Modern applications require sophisticated authorization systems that go beyond traditional role-based access control (RBAC). Here's how relationship-based access control is changing the landscape. Application DB = "What exists" OpenFGA = "Who can do what" -- // RBAC can't easily handle: "Users can edit documents in their department" "Managers can approve expenses under $1000" "Share document with external collaborator" RBAC: System features, admin areas, broad permissions Relationship: Resource sharing, team access, fine-grained control
The Problem: Authorization in Code Most applications handle permissions like this:
// Traditional approach: Authorization mixed with business logic
async function canEditDocument(userId, docId) {
const doc = await db.documents.findById(docId);
const user = await db.users.findById(userId);
return user.isEditor && doc.departmentId === user.departmentId;
}
Problems:
Multiple database queries Logic scattered across codebase Hard to maintain and modify Scales poorly
The Solution: Relationship-Based Authorization
- Define Your Model
// One-time setup: Define relationships
const model = {
schema_version: "1.1",
type_definitions: [{
type: "document",
relations: {
editor: {
this: {
computedUserset: {
object: "department",
relation: "member"
}
}
}
}
}]
};
// When creating a document
await fgaClient.write({
tuples: [{
// Document belongs to department
object: `document:${docId}`,
relation: "department",
user: `department:${departmentId}`
}]
});
// When assigning user to department
await fgaClient.write({
tuples: [{
// User is member of department
user: `user:${userId}`,
relation: "member",
object: `department:${departmentId}`
}]
});
// Your application code becomes this simple
async function canEditDocument(userId, docId) {
const check = await fgaClient.check({
user: `user:${userId}`,
relation: 'editor',
object: `document:${docId}`
});
return check.allowed;
}
Benefits Cleaner Code Single API call for checks Authorization logic centralized Easy to audit and modify Better Performance Optimized permission checking Reduced database load Built for scale Easier Maintenance Change rules without changing code Clear relationship modeling Consistent across applications Getting Started Choose a solution (OpenFGA, Oso, Casbin) Model your basic relationships Migrate highest-value permissions first Gradually expand usage
Real-World Example
// Creating a document with proper authorization
async function createDocument(title, departmentId, creatorId) {
// 1. Create document in your database
const doc = await db.documents.create({ title });
// 2. Store authorization relationships
await fgaClient.write({
tuples: [{
// Creator owns document
user: `user:${creatorId}`,
relation: "owner",
object: `document:${doc.id}`
}, {
// Document belongs to department
object: `document:${doc.id}`,
relation: "department",
user: `department:${departmentId}`
}]
});
return doc;
}
Remember: Think of tuples as "facts" about who can do what. The authorization service uses these facts to answer permission questions.
=========== EXAMPLE 2 =============
// Application DB - documents table
{
id: "doc123",
title: "Q4 Report",
created_by: "user456", // ← Keep this minimal reference
content: "..."
}
// OpenFGA tuples
[
// Authorization relationships
{
user: "user:456",
relation: "owner",
object: "document:123"
},
{
user: "user:456",
relation: "editor",
object: "document:123"
}
]
// Permission check
const canEdit = await fgaClient.check({
user: "user:456",
relation: "editor",
object: "document:123"
});
Created on 1/24/2025