Tekko

Language

Get in Touch

Usually respond within 24 hours

Back to BlogArchitecture

Implementing ReBAC: Building Zanzibar-Style Permissions with OpenFGA and Next.js

7 min read
Next.jsOpenFGAAuthorizationSaaSSecurity
Implementing ReBAC: Building Zanzibar-Style Permissions with OpenFGA and Next.js

As multi-tenant SaaS applications grow in complexity, developers eventually hit a wall with traditional Role-Based Access Control (RBAC). What starts as a simple isAdmin check quickly evolves into a nightmare of nested conditionals: "Can this user view this document? Yes, if they are the owner, OR if they are in the 'Compliance' team, OR if the document is in a folder shared with their department."

This is where Relationship-Based Access Control (ReBAC) comes in. Inspired by Google’s Zanzibar whitepaper, ReBAC shifts the focus from what a user is (their role) to how they are related to a resource. In this article, we will explore how to implement a scalable authorization layer using OpenFGA (Fine-Grained Authorization) within a Next.js application.

The Limitations of RBAC in Modern SaaS

Most developers start with RBAC because it’s easy to reason about. You have a users table and a roles table. To check permissions, you join them. However, RBAC suffers from several architectural limitations in a multi-tenant environment:

  1. Resource Bottlenecks: RBAC is great for global permissions (e.g., can_create_invoice), but poor for resource-specific permissions (e.g., can_view_invoice_#402).
  2. Authorization Debt: As you add more granular rules, your application logic becomes littered with complex SQL joins or recursive permission checks.
  3. Hierarchy Management: Handling nested structures—like a user gaining access to a file because they belong to a sub-team of a parent organization—is computationally expensive and difficult to maintain in a standard relational database.

ReBAC solves this by treating authorization as a graph problem rather than a set of boolean flags.

What is OpenFGA?

OpenFGA is an open-source implementation of the Google Zanzibar model, donated to the CNCF by Okta/Auth0. It allows you to define a relationship model (schema) and then store "tuples" (data points) that represent relationships between users and objects.

Instead of checking if user.role === 'admin', you ask OpenFGA: Does user:123 have relation:viewer on document:xyz? OpenFGA traverses the graph of relationships and returns a simple allowed: true/false.

Designing the Relationship Model (DSL)

Before writing code, we need to define our authorization model using OpenFGA’s Domain Specific Language (DSL). Let's imagine a Google Drive-style application where we have organizations, folders, and documents.

model schema 1.1 type user type organization relations define admin: [user] define member: [user] type folder relations define parent_org: [organization] define owner: [user] define viewer: [user, folder#viewer, organization#member] or owner type document relations define parent_folder: [folder] define owner: [user] define viewer: [user] or owner or viewer from parent_folder

In this model:

  • A document's viewer can be a specific user.
  • A document's viewer can also be inherited from the parent_folder.
  • A folder's viewer can be anyone who is a member of the parent_org.

This "transitive" relationship is the superpower of ReBAC. We don't need to duplicate permission data for every document; we simply point the document to its folder.

Integrating OpenFGA with Next.js

To use OpenFGA in a Next.js environment, we typically interact with the OpenFGA API using their SDK. Since authorization checks should happen on the server, we will focus on implementation within Server Components, Server Actions, and Middleware.

1. Setting up the Client

First, install the SDK:

npm install @openfga/sdk

Then, create a singleton client instance:

// lib/fga.ts import { OpenFgaClient } from '@openfga/sdk'; export const fgaClient = new OpenFgaClient({ apiUrl: process.env.FGA_API_URL, // e.g., http://localhost:8080 storeId: process.env.FGA_STORE_ID, authorizationModelId: process.env.FGA_MODEL_ID, });

2. Checking Permissions in Server Components

When rendering a page in Next.js, you want to ensure the user has access to the data they are requesting. Because OpenFGA is highly optimized, these checks are typically fast enough to run during the request lifecycle.

// app/documents/[id]/page.tsx import { fgaClient } from '@/lib/fga'; import { auth } from '@/lib/auth'; // Your auth solution (NextAuth, Clerk, etc.) import { redirect } from 'next/navigation'; export default async function DocumentPage({ params }: { params: { id: string } }) { const session = await auth(); if (!session) redirect('/login'); const { allowed } = await fgaClient.check({ user: `user:${session.user.id}`, relation: 'viewer', object: `document:${params.id}`, }); if (!allowed) { return <div>Access Denied</div>; } const doc = await db.document.findUnique({ where: { id: params.id } }); return <main>{/* Render document */}</main>; }

3. Securing Server Actions

Server Actions are the primary way to handle mutations in Next.js. Checking permissions here is critical to prevent IDOR (Insecure Direct Object Reference) attacks.

// app/documents/actions.ts 'use server' import { fgaClient } from '@/lib/fga'; import { auth } from '@/lib/auth'; export async function deleteDocument(docId: string) { const session = await auth(); if (!session) throw new Error('Unauthorized'); const { allowed } = await fgaClient.check({ user: `user:${session.user.id}`, relation: 'owner', object: `document:${docId}`, }); if (!allowed) throw new Error('Forbidden'); await db.document.delete({ where: { id: docId } }); // Optional: Clean up the relationship in OpenFGA await fgaClient.write({ deletes: [{ user: `user:${session.user.id}`, relation: 'owner', object: `document:${docId}` }], }); }

Handling Multi-Tenancy and Complex Relationships

One of the biggest challenges in multi-tenant SaaS is "The Global Admin Problem." You often need support staff to access customer data for debugging, but you don't want to manually add them to every folder.

In OpenFGA, you can handle this by adding a support relation to your organization type:

type organization relations define support: [user] define member: [user] or support

Because our document inherits from folder, and folder inherits from organization#member, any user added to the support relation of an organization automatically gains viewer access to every document within that organization. No database migrations required.

Performance and Scalability Considerations

When implementing Zanzibar-style systems, performance is the primary concern. Google Zanzibar was designed to handle trillions of relationships with millisecond latency. While your SaaS might not be at that scale yet, the principles remain:

  1. Check-First Architecture: Always perform the check call as close to the data access as possible. Avoid fetching large lists of IDs and then filtering them in application code.
  2. The List Objects API: If you need to show a list of all documents a user can see, use OpenFGA’s ListObjects API. This returns the list of IDs the user has a specific relation to, which you can then use to query your primary database.
  3. Caching: OpenFGA checks are deterministic for a given set of tuples. However, since tuples change frequently, caching should be handled carefully. Usually, the latency of a local OpenFGA instance (or a managed service like Okta FGA) is low enough that caching is unnecessary at the application level.
  4. Consistency: OpenFGA provides "at least as fresh as" consistency. When you write a relationship (e.g., sharing a file), you get back a contextual_token. You can pass this token to subsequent check calls to ensure the permission update has been propagated.

Why Next.js is the Perfect Frontend for ReBAC

Next.js's move toward a server-first architecture aligns perfectly with centralized authorization. By using Server Components, we eliminate the need to expose our authorization logic to the client-side. The client never sees the "why" behind a permission; it only sees the result of the check call.

Furthermore, the ability to use Middleware allows for coarse-grained checks at the routing level (e.g., ensuring a user belongs to a specific tenant ID based on the URL) before the request even hits your page logic.

Actionable Conclusion

Implementing ReBAC with OpenFGA transforms authorization from a sprawling mess of logic into a clean, centralized, and queryable graph. If you are building a multi-tenant SaaS, stop adding boolean columns to your user table and follow these steps:

  1. Define your model: Use the OpenFGA Playground to visualize your relationships and test your DSL.
  2. Centralize your checks: Use a dedicated client library in your Next.js project to wrap fgaClient.check.
  3. Think in tuples: Every time a resource is created or shared, write a relationship tuple to OpenFGA.
  4. Audit and Scale: Leverage OpenFGA's logs to see exactly why access was granted or denied, providing a level of transparency that traditional RBAC can never match.

By decoupling authorization from your business logic, you ensure that your application remains maintainable and secure, no matter how complex your customer's organizational structures become.