OpenFGA vs Permify vs SpiceDB: Zanzibar-Style Authorization Compared (2026)
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
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.