Skip to main content

OpenFGA vs Permify vs SpiceDB: Zanzibar-Style Authorization Compared (2026)

·PkgPulse Team

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

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.