Skip to main content

Guide

OpenFGA vs Permify vs SpiceDB (2026)

Compare OpenFGA, Permify, and SpiceDB for fine-grained authorization. Relationship-based access control, permission modeling, and which Zanzibar in 2026.

·PkgPulse Team·
0

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

FeatureOpenFGASpiceDBPermify
MaintainerAuth0/OktaAuthZedPermify
LicenseApache 2.0Apache 2.0Apache 2.0
Schema FormatDSL (model)Zed schemaYAML
Zanzibar FidelityHighHighestHigh
Check API
Lookup ResourcesListObjectsLookupResourcesLookupEntity
Lookup SubjectsListUsersLookupSubjectsLookupSubject
Expand API
Watch API✅ (real-time)
Caveats/ConditionsConditions (beta)✅ (Caveats)ABAC rules
ConsistencyEventualConfigurable (ZedTokens)Snapshot tokens
Multi-TenancyStores❌ (namespaces)✅ (built-in)
Storage BackendsPostgres, MySQL, SQLitePostgres, CockroachDB, Spanner, MySQLPostgres, memory
APIgRPC + RESTgRPC + RESTgRPC + REST
Node.js SDK✅ (official)✅ (official)✅ (official)
CLI Toolfgazedpermify
Visual PlaygroundFGA PlaygroundPlayground (web)✅ (built-in UI)
Cloud ManagedOkta FGAAuthZed DedicatedPermify Cloud
ComplexityMediumMedium-HighLow-Medium
Best ForAuth0 ecosystemZanzibar puristsFast 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

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.