Tekko

Language

Get in Touch

Usually respond within 24 hours

Back to BlogSecurity

Scalable ReBAC with OpenFGA and Next.js: A Zanzibar-Style Guide

7 min read
OpenFGANext.jsReBACSaaS ArchitectureAuthorization
Scalable ReBAC with OpenFGA and Next.js: A Zanzibar-Style Guide

The Permissions Wall: Why RBAC Fails Modern SaaS

Most software engineers start their authorization journey with Role-Based Access Control (RBAC). It’s intuitive: a user is an admin, an editor, or a viewer. In a simple application, a middleware check like if (user.role === 'admin') suffices.

However, as a SaaS product scales into the enterprise tier, RBAC inevitably hits a wall. Imagine a multi-tenant project management tool where:

  • Users belong to multiple organizations.
  • Projects are nested within folders.
  • Permissions can be inherited (if you can edit a folder, you can edit all projects inside it).
  • Specific documents can be shared with external collaborators who aren't part of the organization.

Trying to model this with traditional RBAC leads to an explosion of roles (e.g., Folder1_Editor, Project5_Viewer) or a database schema so complex that authorization queries become the primary bottleneck of your application. This is where Relationship-Based Access Control (ReBAC) and Google’s Zanzibar paper come into play.

Understanding ReBAC and the Zanzibar Paradigm

In 2019, Google published the "Zanzibar" paper, describing the global authorization system they use for Drive, YouTube, and Cloud. Instead of checking roles, Zanzibar checks relationships.

ReBAC defines access based on the relationship between a subject (user) and an object (resource). For example: "User:Alice has a viewer relationship with Document:Financial_Report because she is a member of Folder:Q4_Reports."

OpenFGA (Fine-Grained Authorization), an open-source project under the CNCF, provides a high-performance implementation of this model. It allows you to externalize your authorization logic, making it consistent across microservices and scalable across millions of objects.

The Architecture: Next.js and OpenFGA

In a modern Next.js application, we want to ensure that permission checks are fast, secure, and integrated into the request lifecycle. By combining Next.js Server Components and Middleware with OpenFGA, we can create a robust authorization layer that doesn't clutter our business logic.

1. Modeling Your Permissions

Before writing code, we must define our authorization model using the OpenFGA DSL (Domain Specific Language). Let’s model a typical SaaS hierarchy: Organizations contain Folders, and Folders contain Documents.

model schema 1.1 type user type organization relations define admin: [user] define member: [user] or admin type folder relations define parent: [organization] define viewer: [user] or admin from parent define editor: [user] or admin from parent type document relations define parent: [folder] define viewer: [user] or viewer from parent define editor: [user] or editor from parent define can_view: viewer or editor define can_edit: editor

In this model, we see inheritance in action. A document viewer is defined as either a direct user or someone who has a viewer relationship with the document's parent folder. This recursive resolution is handled entirely by OpenFGA, not your application code.

2. Setting Up the OpenFGA Client in Next.js

To interact with OpenFGA, we’ll use the @openfga/sdk. In a Next.js environment, it’s best to initialize this client as a singleton to be used in Server Actions or Route Handlers.

// 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, });

3. Implementing the "Check" Logic

In Next.js, authorization happens at multiple levels. The most critical is at the data fetching layer (Server Components) and the mutation layer (Server Actions).

Protecting a Server Action

When a user attempts to rename a document, we need to verify they have editor rights.

// app/actions/rename-document.ts 'use server' import { fgaClient } from '@/lib/fga'; import { auth } from '@/lib/auth'; // Your authentication provider export async function renameDocument(docId: string, newName: string) { const session = await auth(); if (!session) throw new Error("Unauthorized"); const { allowed } = await fgaClient.check({ user: `user:${session.user.id}`, relation: 'can_edit', object: `document:${docId}`, }); if (!allowed) { throw new Error("You do not have permission to edit this document"); } // Proceed with DB update... }

4. Efficient Data Fetching with ListObjects

One of the hardest problems in ReBAC is the "Search and List" problem. How do you show a user only the documents they have access to without fetching every document and checking them one by one?

OpenFGA provides a ListObjects API for this. Instead of filtering in your application, you ask OpenFGA for the IDs of objects a user can access.

// app/documents/page.tsx import { fgaClient } from '@/lib/fga'; export default async function DocumentsPage() { const session = await auth(); const { objects } = await fgaClient.listObjects({ user: `user:${session.user.id}`, relation: 'can_view', type: 'document', }); // 'objects' will be an array like ["document:1", "document:42"] const docIds = objects.map(o => o.split(':')[1]); const documents = await db.documents.findMany({ where: { id: { in: docIds } } }); return <DocumentList data={documents} />; }

Advanced Pattern: Contextual Tuples

Sometimes, access depends on dynamic data that doesn't live in your authorization store—for example, the user’s IP address or whether the request is made during business hours. OpenFGA supports Contextual Tuples, which allow you to pass temporary relationship data at the moment of the check.

const { allowed } = await fgaClient.check({ user: `user:${userId}`, relation: 'can_access_sensitive_data', object: `organization:${orgId}`, contextualTuples: [ { user: `user:${userId}`, relation: 'is_on_corporate_vpn', object: `network:internal`, } ] });

Performance and Scalability Considerations

When implementing ReBAC in a high-traffic Next.js app, performance is paramount. Every permission check adds latency. Here are three strategies to mitigate this:

1. Strategic Caching

While OpenFGA is highly optimized, you can use the Next.js cache function or a Redis layer to store the results of a check for the duration of a request. However, be extremely careful with TTL (Time To Live). Authorization data is sensitive; a user whose access was revoked should not be able to perform actions for another 5 minutes because of a cache.

2. The "NewerDB" Problem (Consistency)

Zanzibar systems often deal with the "NewerDB" problem: if you add a user to a group and they immediately try to access a resource, the authorization store might be a few milliseconds behind the main database. OpenFGA handles this through consistency tokens, ensuring that your check is performed against a version of the data that includes your latest changes.

3. Middleware vs. Server Components

Do not perform granular OpenFGA checks in Next.js Middleware for every single asset. Middleware should handle coarse-grained checks (e.g., "is the user logged in?"). Use Server Components and Server Actions for fine-grained ReBAC checks to keep the middleware execution time low and avoid unnecessary external calls on static asset requests.

Why This Matters for Multi-Tenant SaaS

In a multi-tenant environment, the "Organization" is the ultimate boundary. By using ReBAC, you can ensure that even if a developer makes a mistake in a SQL query and forgets a WHERE tenant_id = ? clause, the authorization layer acts as a fail-safe. If the user doesn't have a relationship with that object in OpenFGA, the check will return false, regardless of what the database returns.

Furthermore, ReBAC allows you to build "Share" features that enterprise customers demand. You can easily grant a specific contractor access to one folder without inviting them to the whole organization, simply by creating a single relationship tuple in OpenFGA.

Conclusion: Actionable Steps

Implementing ReBAC is a shift in mindset from "Who is this user?" to "How is this user related to this resource?" To get started:

  1. Audit your current permissions: Identify where nested resources or complex inheritance are causing "if/else" bloat in your code.
  2. Start Small: Don't migrate your entire app at once. Pick one resource (e.g., Documents) and model it in the OpenFGA Playground.
  3. Externalize the Source of Truth: Use your primary database for application state, but treat OpenFGA as the source of truth for access.
  4. Integrate with Next.js: Leverage Server Actions for mutations and listObjects for secure data fetching.

By adopting OpenFGA and ReBAC, you are building an authorization system that won't just handle your first 100 users, but will scale to the complexity of the world's largest enterprises.