TL;DR: OpenFGA is the open-source fine-grained authorization engine from Auth0/Okta — flexible authorization model DSL, check/list/expand APIs, and the easiest Zanzibar onramp for teams already using Auth0. SpiceDB is the most Zanzibar-faithful implementation — schema language close to Google's original paper, Watch API for cache invalidation, and strong consistency guarantees. Permify is the developer-friendly authorization service — YAML-based schema, built-in data filtering (lookup), and a visual playground for testing permissions. In 2026: OpenFGA for Auth0 ecosystem and broad language support, SpiceDB for Zanzibar-purist architectures, Permify for developer experience and rapid iteration.
Key Takeaways
- OpenFGA: Open-source (Apache 2.0), by Auth0/Okta. DSL-based authorization model, relationship tuples, Check/ListObjects/ListUsers APIs. SDKs in 8+ languages. Best for teams wanting fine-grained auth with broad ecosystem support
- SpiceDB: Open-source (Apache 2.0), by AuthZed. Schema language closest to Google Zanzibar, Watch API, zed CLI, Caveats (conditional permissions). Best for teams wanting the most faithful Zanzibar implementation with strong consistency
- Permify: Open-source (Apache 2.0). YAML schema, built-in data filtering, visual playground, gRPC + REST APIs. Best for teams wanting fast iteration with visual debugging and a developer-friendly schema format
OpenFGA — Fine-Grained Authorization by Auth0
OpenFGA is the open-source authorization engine from Auth0/Okta — define a model, write relationship tuples, and check permissions with sub-millisecond latency.
Authorization Model
# OpenFGA Authorization Model DSL
model
schema 1.1
type user
type organization
relations
define owner: [user]
define admin: [user] or owner
define member: [user] or admin
type project
relations
define org: [organization]
define owner: [user]
define editor: [user] or owner or admin from org
define viewer: [user] or editor or member from org
# Computed permissions
define can_edit: editor
define can_view: viewer
define can_delete: owner or admin from org
define can_share: editor
type document
relations
define project: [project]
define owner: [user]
define editor: [user] or owner or editor from project
define viewer: [user] or editor or viewer from project
define can_edit: editor
define can_view: viewer
Writing Relationship Tuples
import { OpenFgaClient } from "@openfga/sdk";
const fga = new OpenFgaClient({
apiUrl: "http://localhost:8080",
storeId: process.env.FGA_STORE_ID!,
authorizationModelId: process.env.FGA_MODEL_ID!,
});
// Write relationships
await fga.write({
writes: [
// Jane is owner of Acme org
{ user: "user:jane", relation: "owner", object: "organization:acme" },
// John is a member of Acme org
{ user: "user:john", relation: "member", object: "organization:acme" },
// Project belongs to org
{ user: "organization:acme", relation: "org", object: "project:alpha" },
// Jane owns the project
{ user: "user:jane", relation: "owner", object: "project:alpha" },
// Document belongs to project
{ user: "project:alpha", relation: "project", object: "document:spec" },
// Bob is a direct editor of the document
{ user: "user:bob", relation: "editor", object: "document:spec" },
],
});
// Delete relationships
await fga.write({
deletes: [
{ user: "user:bob", relation: "editor", object: "document:spec" },
],
});
Permission Checks
// Check if a user has permission
const { allowed } = await fga.check({
user: "user:jane",
relation: "can_edit",
object: "document:spec",
});
console.log(`Jane can edit: ${allowed}`); // true (via project owner → org owner)
// Check with context (conditional tuples)
const { allowed: canView } = await fga.check({
user: "user:john",
relation: "can_view",
object: "document:spec",
contextualTuples: [
// Temporary access for review
{ user: "user:john", relation: "viewer", object: "document:spec" },
],
});
// List all objects a user can access
const { objects } = await fga.listObjects({
user: "user:jane",
relation: "can_view",
type: "document",
});
console.log(`Jane can view: ${objects}`);
// ["document:spec", "document:design", ...]
// List all users with access to an object
const { users } = await fga.listUsers({
object: { type: "document", id: "spec" },
relation: "can_edit",
userFilters: [{ type: "user" }],
});
console.log(`Editors: ${users.map((u) => u.object?.id)}`);
Express Middleware
import { OpenFgaClient } from "@openfga/sdk";
function requirePermission(relation: string, getObject: (req: Request) => string) {
return async (req: Request, res: Response, next: NextFunction) => {
const userId = `user:${req.user.id}`;
const object = getObject(req);
const { allowed } = await fga.check({
user: userId,
relation,
object,
});
if (!allowed) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
// Use in routes
app.get(
"/api/documents/:id",
requirePermission("can_view", (req) => `document:${req.params.id}`),
getDocument
);
app.put(
"/api/documents/:id",
requirePermission("can_edit", (req) => `document:${req.params.id}`),
updateDocument
);
app.delete(
"/api/documents/:id",
requirePermission("can_delete", (req) => `document:${req.params.id}`),
deleteDocument
);
SpiceDB — Zanzibar-Faithful Authorization
SpiceDB is the most faithful open-source implementation of Google's Zanzibar — a schema language, relationship storage, and consistency-focused permission checks.
Schema Definition
// SpiceDB Schema — close to Zanzibar's original design
definition user {}
definition organization {
relation owner: user
relation admin: user
relation member: user
// Permissions compose from relations
permission manage = owner
permission administer = admin + owner
permission view = member + administer
}
definition project {
relation org: organization
relation owner: user
relation editor: user
relation viewer: user
permission edit = owner + editor + org->administer
permission view = viewer + edit + org->view
permission delete = owner + org->manage
permission share = edit
}
definition document {
relation project: project
relation owner: user
relation editor: user
relation commenter: user
relation viewer: user
permission edit = owner + editor + project->edit
permission comment = commenter + edit
permission view = viewer + comment + project->view
}
// Caveats — conditional permissions
caveat ip_allowlist(allowed_cidrs list<ipaddress>, user_ip ipaddress) {
user_ip in allowed_cidrs
}
definition sensitive_document {
relation viewer: user with ip_allowlist
permission view = viewer
}
Relationship Management
import { v1 } from "@authzed/authzed-node";
const client = v1.NewClient(
"tc_my_token",
"localhost:50051",
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS
);
// Write relationships
const writeRequest = v1.WriteRelationshipsRequest.create({
updates: [
// Jane owns the organization
v1.RelationshipUpdate.create({
operation: v1.RelationshipUpdate_Operation.TOUCH,
relationship: v1.Relationship.create({
resource: v1.ObjectReference.create({
objectType: "organization",
objectId: "acme",
}),
relation: "owner",
subject: v1.SubjectReference.create({
object: v1.ObjectReference.create({
objectType: "user",
objectId: "jane",
}),
}),
}),
}),
// Project belongs to org
v1.RelationshipUpdate.create({
operation: v1.RelationshipUpdate_Operation.TOUCH,
relationship: v1.Relationship.create({
resource: v1.ObjectReference.create({
objectType: "project",
objectId: "alpha",
}),
relation: "org",
subject: v1.SubjectReference.create({
object: v1.ObjectReference.create({
objectType: "organization",
objectId: "acme",
}),
}),
}),
}),
],
});
const writeResponse = await client.writeRelationships(writeRequest);
const zedToken = writeResponse.writtenAt; // ZedToken for consistency
Permission Checks with Consistency
// Check permission with full consistency
const checkRequest = v1.CheckPermissionRequest.create({
resource: v1.ObjectReference.create({
objectType: "document",
objectId: "spec",
}),
permission: "edit",
subject: v1.SubjectReference.create({
object: v1.ObjectReference.create({
objectType: "user",
objectId: "jane",
}),
}),
consistency: v1.Consistency.create({
// Ensure check reflects all writes up to this token
atLeastAsFresh: zedToken,
}),
});
const checkResponse = await client.checkPermission(checkRequest);
const allowed =
checkResponse.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
// Check with caveat context
const caveatedCheck = v1.CheckPermissionRequest.create({
resource: v1.ObjectReference.create({
objectType: "sensitive_document",
objectId: "financials",
}),
permission: "view",
subject: v1.SubjectReference.create({
object: v1.ObjectReference.create({
objectType: "user",
objectId: "jane",
}),
}),
context: v1.Struct.fromJSON({
user_ip: "10.0.1.50",
}),
});
// Lookup resources — what can this user access?
const lookupRequest = v1.LookupResourcesRequest.create({
resourceObjectType: "document",
permission: "view",
subject: v1.SubjectReference.create({
object: v1.ObjectReference.create({
objectType: "user",
objectId: "jane",
}),
}),
});
const stream = client.lookupResources(lookupRequest);
for await (const response of stream) {
console.log(`Can view: document:${response.resourceObjectId}`);
}
Watch API — Cache Invalidation
// Watch for permission changes — real-time cache invalidation
const watchRequest = v1.WatchRequest.create({
optionalObjectTypes: ["document", "project"],
optionalStartCursor: lastKnownZedToken,
});
const watchStream = client.watch(watchRequest);
for await (const response of watchStream) {
for (const update of response.updates) {
const resource = `${update.relationship!.resource!.objectType}:${update.relationship!.resource!.objectId}`;
const subject = `${update.relationship!.subject!.object!.objectType}:${update.relationship!.subject!.object!.objectId}`;
console.log(`${update.operation}: ${subject} → ${resource}`);
// Invalidate permission cache for affected resources
await cache.invalidate(resource);
}
}
Permify — Developer-Friendly Authorization
Permify is the developer-friendly authorization service — YAML-based schema, visual playground, and built-in data filtering for list queries.
Schema Definition
# Permify Schema — YAML-based for readability
entity user {}
entity organization {
relation owner @user
relation admin @user
relation member @user
action manage = owner
action administer = admin or owner
action view_members = member or administer
}
entity project {
relation org @organization
relation owner @user
relation editor @user
relation viewer @user
action edit = owner or editor or org.administer
action view = viewer or edit or org.view_members
action delete = owner or org.manage
action share = edit
}
entity document {
relation project @project
relation owner @user
relation editor @user
relation commenter @user
relation viewer @user
action edit = owner or editor or project.edit
action comment = commenter or edit
action view = viewer or comment or project.view
}
Writing Relationships
// Permify gRPC client
import { grpc } from "@permify/permify-node";
const client = new grpc.newClient({
endpoint: "localhost:3478",
});
// Write relationships
await client.data.write({
tenantId: "t1",
metadata: {
schemaVersion: "", // latest
},
tuples: [
{
entity: { type: "organization", id: "acme" },
relation: "owner",
subject: { type: "user", id: "jane" },
},
{
entity: { type: "organization", id: "acme" },
relation: "member",
subject: { type: "user", id: "john" },
},
{
entity: { type: "project", id: "alpha" },
relation: "org",
subject: { type: "organization", id: "acme" },
},
{
entity: { type: "document", id: "spec" },
relation: "project",
subject: { type: "project", id: "alpha" },
},
{
entity: { type: "document", id: "spec" },
relation: "editor",
subject: { type: "user", id: "bob" },
},
],
});
Permission Checks
// Check permission
const checkResult = await client.permission.check({
tenantId: "t1",
metadata: {
schemaVersion: "",
snapToken: "", // latest snapshot
depth: 20, // max traversal depth
},
entity: { type: "document", id: "spec" },
permission: "edit",
subject: { type: "user", id: "jane" },
});
console.log(`Jane can edit: ${checkResult.can}`);
// RESULT_ALLOWED or RESULT_DENIED
// Lookup — find all entities user can access (data filtering)
const lookupResult = await client.permission.lookupEntity({
tenantId: "t1",
metadata: { schemaVersion: "", snapToken: "", depth: 20 },
entityType: "document",
permission: "view",
subject: { type: "user", id: "jane" },
});
console.log(`Jane can view: ${lookupResult.entityIds}`);
// ["spec", "design", "roadmap", ...]
// Lookup subjects — who has access to this resource?
const subjectResult = await client.permission.lookupSubject({
tenantId: "t1",
metadata: { schemaVersion: "", snapToken: "", depth: 20 },
entity: { type: "document", id: "spec" },
permission: "edit",
subjectReference: { type: "user" },
});
console.log(`Editors: ${subjectResult.subjectIds}`);
// ["jane", "bob", ...]
// Subject filter — check permission for multiple subjects
const filterResult = await client.permission.subjectPermission({
tenantId: "t1",
metadata: { schemaVersion: "", snapToken: "", depth: 20 },
entity: { type: "document", id: "spec" },
subject: { type: "user", id: "jane" },
});
// Returns all permissions jane has on this document
console.log(filterResult.results);
// { edit: ALLOWED, comment: ALLOWED, view: ALLOWED }
Multi-Tenancy
// Permify has built-in multi-tenancy
// Each tenant has isolated schemas and data
// Create tenant
await client.tenancy.create({ id: "customer-acme", name: "Acme Corp" });
await client.tenancy.create({ id: "customer-globex", name: "Globex Corp" });
// Write schema per tenant (or share schemas)
await client.schema.write({
tenantId: "customer-acme",
schema: acmeSchema,
});
// All operations are tenant-scoped
await client.permission.check({
tenantId: "customer-acme", // Isolated
entity: { type: "document", id: "spec" },
permission: "edit",
subject: { type: "user", id: "jane" },
});
Feature Comparison
| Feature | OpenFGA | SpiceDB | Permify |
|---|---|---|---|
| Maintainer | Auth0/Okta | AuthZed | Permify |
| License | Apache 2.0 | Apache 2.0 | Apache 2.0 |
| Schema Format | DSL (model) | Zed schema | YAML |
| Zanzibar Fidelity | High | Highest | High |
| Check API | ✅ | ✅ | ✅ |
| Lookup Resources | ListObjects | LookupResources | LookupEntity |
| Lookup Subjects | ListUsers | LookupSubjects | LookupSubject |
| Expand API | ✅ | ✅ | ✅ |
| Watch API | ❌ | ✅ (real-time) | ❌ |
| Caveats/Conditions | Conditions (beta) | ✅ (Caveats) | ABAC rules |
| Consistency | Eventual | Configurable (ZedTokens) | Snapshot tokens |
| Multi-Tenancy | Stores | ❌ (namespaces) | ✅ (built-in) |
| Storage Backends | Postgres, MySQL, SQLite | Postgres, CockroachDB, Spanner, MySQL | Postgres, memory |
| API | gRPC + REST | gRPC + REST | gRPC + REST |
| Node.js SDK | ✅ (official) | ✅ (official) | ✅ (official) |
| CLI Tool | fga | zed | permify |
| Visual Playground | FGA Playground | Playground (web) | ✅ (built-in UI) |
| Cloud Managed | Okta FGA | AuthZed Dedicated | Permify Cloud |
| Complexity | Medium | Medium-High | Low-Medium |
| Best For | Auth0 ecosystem | Zanzibar purists | Fast iteration |
When to Use Each
Choose OpenFGA if:
- You're in the Auth0/Okta ecosystem and want integrated authorization
- You need SDKs in many languages (Go, JS, Python, Java, C#, etc.)
- ListObjects and ListUsers APIs are important for your UI (show accessible resources)
- You want the broadest community support and documentation
- Condition-based authorization (beta) fits your ABAC needs
Choose SpiceDB if:
- You want the most faithful Google Zanzibar implementation
- The Watch API for real-time cache invalidation is important
- Caveats (conditional permissions like IP allowlists, time-based access) are required
- Strong consistency guarantees (ZedTokens) matter for your security model
- You need CockroachDB or Spanner as a backend for global distribution
Choose Permify if:
- YAML-based schema definition is easier for your team to read and maintain
- Built-in visual playground for testing permissions accelerates development
- Multi-tenancy is a first-class requirement (SaaS with per-customer isolation)
- SubjectPermission API (check all permissions at once) reduces API calls
- You want the fastest development iteration cycle
Production Performance and Latency Considerations
Authorization system latency directly affects every API response time since permission checks occur in the critical path of request handling. All three systems are designed for sub-millisecond check latency under normal conditions, but the actual latency depends on the complexity of the relationship graph traversal required to answer the check. A simple check like "does user X have relation Y to object Z" with no transitive relationships resolves in a single database lookup. Complex checks traversing multiple hops — "can user X edit document Z, which requires editor permission on the project, which requires membership in the organization" — require multiple sequential lookups and increase latency proportionally. OpenFGA caches relationship tuples in memory for frequently accessed objects, reducing repeat check latency significantly. SpiceDB's ZedToken consistency model allows reads from slightly stale replicas for improved throughput — accepting eventual consistency for the performance gain. Permify's snapshot token system provides similar latency-throughput trade-offs. At very high scale (millions of checks per second), running the authorization service as a sidecar to your application pods eliminates network hop latency entirely.
Data Modeling Complexity and Schema Design
The quality of your permission model determines whether the system remains comprehensible as it scales. All three systems implement Google Zanzibar's relationship-based access control (ReBAC), where permissions derive from a graph of typed relationships between objects. The key design challenge is avoiding relationship tuple explosion — storing one tuple per user-resource pair at scale creates storage and query performance problems. The solution in all three systems is using group/organization memberships as intermediate nodes: rather than storing "user X can view document D" for every user in an organization, store "user X is member of org O" and "org O has access to document D," reducing tuples to N+M rather than N×M. Computed permissions (in OpenFGA as derived relations, in SpiceDB as permission = relation1 + relation2->relation3, in Permify as action = role1 or role2) let you express policy in terms of primitive relationships, keeping the tuple store normalized. Well-designed models using these patterns scale efficiently; poorly designed models with redundant tuple storage become operational burdens.
Self-Hosting Architecture and Storage Requirements
All three systems require a persistent storage backend for relationship tuples and a caching layer for performance. OpenFGA supports PostgreSQL, MySQL, SQLite (development only), and an in-memory store for testing. The production architecture is typically OpenFGA servers behind a load balancer with a PostgreSQL RDS instance, scaled horizontally since OpenFGA servers are stateless. SpiceDB supports PostgreSQL, CockroachDB, MySQL, and Spanner for global distribution, with CockroachDB being the recommended choice for multi-region deployments since it handles distributed writes natively. Permify's primary storage backend is PostgreSQL with memory fallback for development. All three benefit from a Redis or Memcached caching layer to reduce repeated tuple lookups for hot objects. The storage footprint is proportional to the number of relationship tuples: for a SaaS application with 10,000 users and 100,000 documents, expect tens of millions of tuples and tens of gigabytes of storage at scale. Planning for this growth from the start prevents painful migrations later.
Migration from RBAC to ReBAC
Many teams adopt these Zanzibar-style systems as a migration from traditional role-based access control, and understanding the migration path prevents business logic gaps during the transition. The conceptual shift from "user has role admin" to "user has relation admin on organization" is subtle but significant — ReBAC roles are always contextual to specific resources rather than global. During migration, both systems often run in parallel: the legacy RBAC check fires first, and if it grants access, the request proceeds; the ReBAC system also evaluates the check in the background, logging discrepancies. This shadow mode validates that the new permission model matches the old behavior before cutting over. OpenFGA's contextual tuples feature is particularly useful here — you can pass temporary relationship tuples in the check request that don't exist in the permanent store, allowing you to test "what if this user had this relationship" scenarios during model validation without writing tuples to production.
Compliance and Audit Logging
Authorization systems are compliance-relevant infrastructure — who accessed what, when, and with what permissions is often a regulatory requirement. SpiceDB's Watch API streams all relationship tuple changes in real time, making it straightforward to build an audit log of permission changes. OpenFGA does not currently provide a built-in Watch API, requiring application-level event logging for audit purposes. Permify similarly relies on application-level logging of permission changes. All three systems' check and write APIs should be instrumented with structured logging that records the user, action, resource, and decision for each authorization check — this creates the access audit log required by SOC 2, ISO 27001, and similar frameworks. The authorization service itself should not be the system of record for this audit log; events should be forwarded to a dedicated audit logging system (CloudTrail, a SIEM, or a dedicated audit database) that retains them for the required period with tamper-evident storage.
Methodology
Feature comparison based on OpenFGA v1.x, SpiceDB v1.x, and Permify v1.x documentation as of March 2026. All three implement Google Zanzibar's relationship-based access control model. Code examples use official Node.js/TypeScript SDKs. Evaluated on: schema expressiveness, API completeness, consistency model, multi-tenancy, and developer experience.
See also: CASL vs Casbin vs accesscontrol 2026, Cerbos vs Permit.io vs OPA 2026, and bcrypt vs Argon2 vs scrypt