ReBAC Authorization: Zanzibar, OpenFGA vs SpiceDB vs Cedar Architecture (2026)

ReBAC Authorization: Zanzibar, OpenFGA vs SpiceDB vs Cedar Architecture (2026)

Relationship-Based Access Control: Zanzibar, OpenFGA vs SpiceDB vs Cedar

Every product eventually writes the same bug. A user shares a folder, the sharing logic is a join across four tables, and six months later someone adds “organization admins can see everything” in a controller that nobody remembers to update in the search endpoint. Role checks do not describe this world. Access in a modern SaaS or industrial platform is a property of a graph: this person is in that team, which owns this site, which contains these devices. That is exactly what relationship-based access control (ReBAC) models, and it is why Google built Zanzibar and why OpenFGA, SpiceDB, and Cedar now dominate the conversation in 2026.

The three projects are not interchangeable. Two of them are Zanzibar-style relationship databases with different consistency stories; the third is a policy language that deliberately stores nothing. Choosing wrong means either a rewrite or a latency budget you cannot meet.

What this covers: the Zanzibar design in mechanism-level detail, how OpenFGA and SpiceDB implement it, where Cedar fits and where it does not, a decision matrix, runnable examples, failure modes, and a hybrid architecture that most real systems end up with.

Context and Background

Authorization models have evolved in three broad layers. Role-based access control (RBAC) assigns permissions to roles and roles to users; it is simple and auditable, but it explodes when the number of resources grows, because “editor of document 8841” is not a role anyone wants to create. Attribute-based access control (ABAC) evaluates predicates over attributes of the principal, action, resource, and environment; it is expressive but gives you no natural way to say “anyone who can view the parent folder can view this file.” ReBAC fills that gap by making the relationships themselves the data, and by letting permissions be defined as traversals over those relationships.

The canonical reference is Google’s Zanzibar paper, presented at USENIX ATC 2019. According to the authors, the system stores trillions of access control lists, serves millions of authorization requests per second, kept 95th-percentile latency under 10 milliseconds, and held availability above 99.999 percent over three years of production use. It backs Google services such as Drive, Photos, Calendar, Cloud, and YouTube. The paper is public on the USENIX site, and nearly every ReBAC product since cites it as its design source.

Open-source implementations followed. OpenFGA was created at Auth0 (now part of Okta), open sourced in 2022, donated to the Cloud Native Computing Foundation (CNCF), and according to the project’s own page moved from Sandbox to Incubating on October 28, 2025. SpiceDB, built by AuthZed, is an Apache-licensed Zanzibar-inspired database whose release stream was at v1.57.0 as of this month, going by the project’s GitHub releases page. Cedar is different in kind: AWS architected it as a policy language for Amazon Verified Permissions, and it joined the CNCF Sandbox, with InfoQ reporting the move in late January 2026.

If you already understand how cloud identity policy is evaluated, the contrast is useful. Our walk-through of AWS IAM policy evaluation logic, SCPs, and permission boundaries covers a policy engine built for a few thousand principals in an account. Application-level authorization has a different shape: millions of end users, billions of resources, and checks inside the request path of every API call.

The thesis of this article is simple. These tools answer two separate questions. Zanzibar descendants answer “is there a path between this subject and that object, and is my view of the graph fresh enough?” Cedar answers “given everything I was handed about this request, do my policies permit it?” Treating them as competitors is the most common architecture mistake in the space. Treating them as layers is usually the right fix.

Zanzibar Reference Architecture for Relationship-Based Access Control

Direct answer: Zanzibar represents permissions as relation tuples of the form object, relation, subject. A namespace configuration then rewrites relations into other relations (for example, “viewer includes editor, and includes viewers of the parent folder”). A check is a graph reachability query over those tuples, evaluated at a consistent snapshot identified by a token the client carries.

Zanzibar-style relationship-based access control architecture showing check API, aggregator, dispatch, tuple store, and index

Figure 1: Anatomy of a Zanzibar-style ReBAC system. Checks fan out across peer nodes, rewrite rules expand relations, and a changelog feeds both Watch consumers and the deep-group index.

Figure 1 shows the shape. The client calls a Check API on any node. That node acts as an aggregator: it checks a cache, then dispatches subproblems to peer nodes responsible for the relevant parts of the graph, evaluates userset rewrites, and reads tuples from the datastore. Writes go to the same store and append to a changelog that downstream systems and an index consume.

Tuples and namespaces

The core data model is tiny. A tuple reads document:readme#viewer@user:alice, meaning user alice has the viewer relation on the document readme. The subject can itself be a set, such as team:platform#member, which means “everyone who is a member of platform.” That second form, a userset, is what makes the model a graph rather than a flat ACL list. Groups of groups, folders within folders, and organizations containing teams are all expressed by pointing one tuple’s subject at another object’s relation.

A namespace configuration defines, per object type, which relations exist and how each is computed. Two rewrite primitives do most of the work. A computed userset says that one relation implies another on the same object: every editor is a viewer. A tuple-to-userset says to follow a relation to a related object and inherit a relation there: a document’s viewers include the viewers of whichever folder is its parent. The paper also describes set operations, union, intersection, and exclusion, so you can write “viewer but not blocked.”

Why does this matter architecturally? Because permission logic moves out of application code and into data plus a small declarative schema. Adding “contractors can comment but never edit” becomes a schema change, not a code review across fifteen services. The cost is that the schema becomes critical infrastructure: a mistaken rewrite grants access everywhere at once.

Evaluating a check

A check asks whether a subject holds a relation on an object. The engine expands the relation using the rewrite rules into a tree of subproblems: look up direct tuples, expand each computed userset, follow each tuple-to-userset edge, and recurse into usersets that appear as subjects. Leaves resolve to tuple reads; internal nodes combine results with union, intersection, or exclusion.

Done naively this walks a large part of the graph. Zanzibar’s answer is parallelism and short-circuiting. Union nodes can return true as soon as one child does, and the paper describes issuing the subqueries concurrently. Dispatch is also sharded by object so that repeated work for hot objects lands on the same node, where a cache is warm. Section 3.2.5 style optimisations in the paper, such as request hedging (sending a duplicate request to another replica if one is slow) trim the tail latency that otherwise dominates a fan-out system.

Consistency without a global lock: zookies

The most distinctive idea in the paper is the “new enemy problem.” Suppose Alice removes Bob from a document and then adds sensitive content. If a replica evaluates a later check against a stale snapshot taken before Bob’s removal, Bob sees the content. Conversely, if ordering across ACL updates and content updates is lost, access decisions can be wrong in a way that no amount of caching discipline fixes.

Zanzibar’s solution is a consistency token it calls a zookie. After a content change, the application asks Zanzibar for a zookie reflecting the time of that change and stores it alongside the content. Later checks on that content pass the zookie, and Zanzibar guarantees evaluation at a snapshot at least as fresh as that token. Because Google’s store is Spanner, whose TrueTime-based timestamps give externally consistent commit ordering, the token can be simply a timestamp. The system picks a snapshot “at least as fresh” as the zookie but otherwise as old as is convenient, which keeps most reads served from local replicas and caches.

This is the property that separates a real Zanzibar descendant from “a graph query on top of Postgres.” Without a story for the new enemy problem, you have a fast authorization cache with an unbounded staleness window.

Leopard and deep groups

Pure recursion breaks down when group nesting is deep or groups are huge. Zanzibar addresses this with a specialised indexing system, Leopard, which precomputes flattened set membership so that checks like “is Alice in this group, directly or transitively?” become set-intersection lookups rather than graph walks. Leopard consumes the same changelog that Watch exposes, keeps an offline-built index plus an incremental layer, and is the reason deeply nested organisations remain tractable at Google scale.

Most open-source implementations do not ship a Leopard equivalent as a default open feature. SpiceDB’s vendor sells a commercial indexing capability for this problem; its experimental full relation cache in recent releases takes a related route by caching the complete relationship set for a hot relation at a given revision. Open-source users should plan for depth limits and measure early.

Implementing ReBAC: OpenFGA, SpiceDB, and Cedar Side by Side

Direct answer: OpenFGA and SpiceDB both implement the Zanzibar tuple-and-rewrite model with their own schema languages and APIs. SpiceDB follows the paper’s consistency tokens closely. OpenFGA offers simpler operations and consistency modes without zookies. Cedar is a stateless policy language that evaluates supplied entity data and does not store relationships.

Sequence diagram of a relationship-based access control write followed by a check carrying a consistency token

Figure 2: The write-then-check sequence. The consistency token returned by a write is stored with the protected resource and presented on later checks to prevent the new enemy problem.

Figure 2 is the flow that the consistency discussion above prescribes. It is what SpiceDB exposes natively; with OpenFGA you accept a different trade-off, discussed below.

OpenFGA

OpenFGA models are written in a compact DSL. Here is a document-sharing model that mirrors the Drive example from the paper:

model
  schema 1.1

type user

type team
  relations
    define member: [user]

type folder
  relations
    define owner: [user]
    define parent: [folder]
    define viewer: [user, team#member] or owner or viewer from parent

type document
  relations
    define parent: [folder]
    define editor: [user, team#member]
    define viewer: [user, team#member] or editor or viewer from parent
    define can_view: viewer
    define can_edit: editor

The line viewer from parent is a tuple-to-userset. Writing tuples and checking looks like this over the HTTP API (the store ID and model ID are placeholders):

curl -s -X POST "$FGA_URL/stores/$STORE_ID/write" \
  -H "content-type: application/json" \
  -d '{"writes":{"tuple_keys":[
        {"user":"user:alice","relation":"member","object":"team:platform"},
        {"user":"team:platform#member","relation":"viewer","object":"folder:roadmap"},
        {"user":"folder:roadmap","relation":"parent","object":"document:q4-plan"}]}}'

curl -s -X POST "$FGA_URL/stores/$STORE_ID/check" \
  -H "content-type: application/json" \
  -d '{"tuple_key":{"user":"user:alice","relation":"can_view","object":"document:q4-plan"}}'
# {"allowed":true}

Alice reaches the document through team membership, folder viewership, and folder parentage: three hops she never had a direct tuple for.

Beyond Check, OpenFGA provides ListObjects (which objects of a type can this user access?), ListUsers, Expand, and batch checking. It supports conditional tuples evaluated with Google’s Common Expression Language (CEL), contextual tuples supplied at request time, and datastores including in-memory, Postgres, and MySQL according to the project documentation; confirm the current list on openfga.dev before you commit, since backends evolve. Its operational posture is “easy to run and embed”: a single Go binary, a Helm chart, and SDKs in many languages. It is an incubating CNCF project, which signals governance maturity but is not a guarantee of production fitness for your workload.

Consistency in OpenFGA is configurable per request, with a preference between lower latency and higher consistency, rather than a Zanzibar-style token handed back from each write. That is a deliberate simplification. It works well when your product can tolerate a short window between a revoke and its visibility, and poorly when a revoke must be reflected on the very next read of sensitive content.

SpiceDB

SpiceDB’s schema language is explicit about the distinction between relations (stored data) and permissions (computed). The same example:

definition user {}

definition team {
    relation member: user
}

definition folder {
    relation owner: user
    relation parent: folder
    relation viewer: user | team#member
    permission view = viewer + owner + parent->view
}

definition document {
    relation parent: folder
    relation editor: user | team#member
    relation viewer: user | team#member
    permission edit = editor
    permission view = viewer + editor + parent->view
}

The arrow parent->view is the tuple-to-userset. Separating relation from permission is a real design improvement over the paper’s flat model: applications check permissions, never raw relations, so you can refactor how a permission is derived without changing a single call site.

The consistency model is where SpiceDB follows Zanzibar most faithfully. Every write returns a ZedToken. Reads can request minimize latency, at least as fresh as a given token, at exact snapshot, or fully consistent. With zed, the CLI:

zed relationship create team:platform member user:alice
zed relationship create folder:roadmap viewer team:platform#member
zed relationship create document:q4-plan parent folder:roadmap
zed permission check document:q4-plan view user:alice
# true
zed permission check document:q4-plan view user:alice --consistency-at-least "$ZEDTOKEN"

Beyond Check, SpiceDB offers LookupResources and LookupSubjects for reverse queries, bulk check APIs, a Watch API over the changelog, and CEL-based caveats for conditional relationships. Supported datastores per its documentation include Postgres, CockroachDB, Cloud Spanner, and MySQL, plus an in-memory store for tests. Postgres and CockroachDB are the common paths; Spanner brings you closest to the original architecture. The v1.57.0 release notes, which I checked on the project’s releases page, show the project still shipping substantive scalability work: an opt-in experimental full relation cache, a resumable bulk DeleteRelationships with cursors, and a bulk delete command that is resumable on CockroachDB. Treat the experimental flag as exactly that.

Cedar

Cedar looks nothing like those two. A Cedar policy is a rule:

permit (
  principal in Group::"platform",
  action in [Action::"view", Action::"comment"],
  resource in Folder::"roadmap"
)
when { resource.classification != "restricted" };

forbid (
  principal,
  action,
  resource
)
unless { context.device_compliant == true };

Cedar semantics are strict and small. Requests are denied by default; any matching forbid overrides any matching permit; evaluation order does not matter. The language is formally specified, and according to AWS and the InfoQ report the specification is verified with the Lean theorem prover and the Rust implementation is tested differentially against that formal model. A validator checks policies against a schema before deployment, and the analysis tooling can answer questions like “can any policy ever let a non-admin delete?” That analyzability is a genuine differentiator that neither Zanzibar descendant offers.

What Cedar does not have is a relationship store. Entity hierarchies (principal in Group::"platform") are part of the data you pass in with each authorization request, or that a host such as Amazon Verified Permissions supplies from its own store. Cedar can express ReBAC-style rules over whatever entity graph you hand it, and the project describes support for RBAC, ABAC, and ReBAC, but it does not index billions of tuples, offer reverse lookups across them, or provide consistency tokens. Anyone using Cedar for graph-heavy authorization needs to build the graph store and the “which entities are relevant to this request” slicer themselves.

A decision matrix

Dimension OpenFGA SpiceDB Cedar
Core idea Zanzibar-style tuples and rewrites Zanzibar-style tuples and rewrites Stateless policy language over supplied entities
Stores relationships Yes Yes No (caller or host supplies entity data)
Consistency tokens Per-request consistency preference, no zookie-style token ZedTokens with four consistency modes Not applicable
Reverse queries (list what I can see) ListObjects, ListUsers LookupResources, LookupSubjects Not native
Conditions CEL conditions CEL caveats Full policy conditions, forbid and permit
Analysis and verification Model validation Schema validation and tooling Formal specification and automated policy analysis
Ops posture Single binary, simple More knobs, multiple datastores Library embedded in your app, or managed service
Governance CNCF Incubating Vendor-led open source, commercial offering CNCF Sandbox
Best fit Product teams wanting fast adoption Teams needing strict consistency and scale Attribute guardrails, policy audit, compliance

Read the table as a map of questions rather than a ranking. A system that must prove why a decision was made to an auditor values Cedar’s analyzability. A system with fifty million documents shared in arbitrary graphs needs a tuple store.

Deeper Analysis: Capacity, Consistency, and Choosing

Direct answer: Pick by the shape of your questions. If you must answer “list everything this user can access” over a large graph with revocation guarantees, use a Zanzibar-style store with token-based consistency. If decisions depend on request attributes and need provable guardrails, use Cedar. Most mature systems use both in sequence.

Decision flowchart for choosing between OpenFGA, SpiceDB, and Cedar for fine-grained authorization

Figure 3: A decision path. Graph-shaped access with large reverse queries pushes you to a tuple store; strictness of revocation separates SpiceDB from OpenFGA; attribute guardrails layer on top via Cedar.

The three cost centres of ReBAC

Three costs dominate production ReBAC, and none appears in a README.

The first is graph depth. A check is a traversal, and its latency scales with the number of hops and the fan-out at each hop. A document in a folder in a folder in a project in an organisation costs four tuple-to-userset steps. A group with 200,000 members nested inside another group costs far more if the engine expands membership rather than testing it. Design schemas to keep resource hierarchies shallow, flatten organisation structure where you can, and denormalise ruthlessly for the common case, for example by writing a direct “tenant member” tuple at signup instead of deriving it three levels up.

The second is write amplification and reverse lookup. Every share is a tuple write, but list queries are where cost hides. “Show me every document I can see” has to enumerate candidates and check each, or run a reverse traversal, and the result sets can be huge. Both OpenFGA’s ListObjects and SpiceDB’s LookupResources are the most expensive calls in their APIs. A frequent production pattern is to avoid them in hot paths: query your own database with a pre-filter, then batch-check the candidate page against the authorization system. That pushes authorization to “filter a page of 50 rows,” a bounded cost, instead of “enumerate the universe.”

The third is consistency. The new enemy problem is easy to dismiss until it bites: a user is removed from a project, an in-flight export job runs with a stale decision, and sensitive data leaves. If your product has any case where “revoke now” must be honoured on the very next read, you either need token-based consistency or you need to run checks in a fully consistent mode and pay the latency. SpiceDB’s design makes this explicit. With OpenFGA you choose per request between a latency-optimised mode and a higher-consistency mode, and you should test what that means on your chosen datastore and cache configuration. I have not benchmarked either system, and I will not quote numbers here; compare them on your own tuple shapes.

Throughput and capacity: how to reason without invented benchmarks

Published numbers for these systems depend heavily on schema shape, hardware, datastore, and cache hit rates, so a table of “checks per second” would be misleading. Instead, reason in terms of tuple store reads per check. A flat check with a direct tuple costs one indexed read. A check with a three-level folder hierarchy costs roughly one read per level in the worst case, multiplied by branching in union expressions. If your cache hit rate is 90 percent on hot objects, the datastore sees one tenth of that load.

Do a back-of-envelope capacity plan: peak API requests per second, times authorization checks per request (typically between one and several, more if the endpoint returns lists), times average reads per uncached check, times the cache miss rate. The result is the read load on your tuple datastore. Then size the datastore for it, and add headroom for the write path and for Watch consumers. If the number looks alarming, the lever is almost always schema flattening and caching, not a faster database.

Latency budgets deserve the same arithmetic. Authorization is serial in the request path. If your service budget is 100 ms at p99 and the authorization call can consume 10 percent, then a single check has to finish in about 10 ms at p99 including the network hop. Zanzibar reports p95 under 10 ms at Google scale with extensive caching and its own replicated Spanner deployment. A single-region Postgres-backed open-source deployment can meet comparable budgets for shallow schemas with warm caches, but you should verify this under your own load, and plan for co-locating the authorization service with the application.

Multi-tenancy and blast radius

Tenancy design shapes everything. Two patterns appear. In shared-store multi-tenancy, all tenants share one schema and one tuple space, with every object ID prefixed or namespaced by tenant. This is operationally cheap and gives you a single place to evolve the schema, but one bad write path or a poisoned hot object affects everyone. In store-per-tenant designs, which OpenFGA’s “store” concept encourages and SpiceDB achieves with separate deployments or prefixes, isolation is cleaner and deletion is trivial, but schema migrations multiply and you lose cross-tenant queries.

For regulated customers, per-tenant stores buy a compliance story: you can prove one tenant’s relationships live in one logical store, and offboarding is a delete. For a consumer product with millions of small tenants, per-tenant stores are impractical, and you rely on strict ID namespacing plus tests that assert no tuple ever bridges tenants.

Schema evolution and migrations

Schemas are code with a data migration problem. Adding a new relation is safe. Changing how a permission is computed changes access for existing data, instantly, in every environment where the new schema is active. Treat schema changes like database migrations: write them as a pull request, run a test suite of assertions (both products support assertion or validation files for expected allow and deny pairs), and deploy with a shadow evaluation step where you compute both old and new results for a sample of live checks and diff them before cutting over.

A useful discipline is “relations are nouns, permissions are verbs.” Store facts such as owner, member, and parent as relations, and compute everything the application asks about as a permission. SpiceDB enforces this split syntactically; in OpenFGA you adopt it as a convention by naming can_view style relations. Either way, application code should never check a raw relation, because it welds your product to your current data layout.

Where Cedar earns its place

Cedar’s strength shows when decisions depend on context that does not belong in a graph. Device compliance, time of day, tenant plan, data classification, and request origin are attributes of the situation, not edges in a relationship graph. Putting them into tuples forces you to write and revoke tuples as these facts change, which is awkward and slow. A policy that reads context.device_compliant has no write path at all.

Cedar also gives you a property that is increasingly demanded: provable negation. A single forbid rule can enforce “nobody ever exports restricted data from an unmanaged device,” and the analysis tooling can check that no combination of permits overrides it. In Zanzibar-style systems, negation is possible through exclusion in the schema, but proving that no path anywhere circumvents it is a graph property you must test, not a theorem you can ask the tool to prove.

Cedar’s costs are the mirror image. You must gather the entity data for each request, which means a lookup in your own database or a managed store to materialise the principal’s groups and the resource’s ancestors. For graph-heavy hierarchies that is exactly the work a ReBAC engine does for you. For listing, partial evaluation features exist in the ecosystem, but I treat their maturity as something to verify against current Cedar documentation before depending on them.

Hybrid Architecture: ReBAC for Who, Policy for When

Hybrid authorization architecture with Cedar guardrails in front of a ReBAC check

Figure 4: A layered pattern. Cedar enforces attribute guardrails and forbids first; a relationship check then confirms there is a path from subject to object. Both must pass.

The layered design in Figure 4 is the one I see most often in mature platforms, and it follows from the earlier argument. Put coarse, attribute-driven, audit-critical rules into Cedar and let them run first, because they are cheap, stateless, and can short-circuit with a deny. Then ask the relationship engine whether there is a path. An allow requires both.

Consider an industrial IoT example. A technician should be able to read telemetry for any asset at a site their team services. That is a graph question: user is a member of a team, the team services a site, the site contains an asset, the asset exposes a stream. It would be miserable to encode as role grants per asset, and trivial as tuples. But a second class of rules is about context: no remote firmware writes outside a maintenance window, no access to safety-critical controllers from unmanaged laptops, no exports of data tagged export-controlled. Those are attribute rules, and they must hold regardless of how generous the graph is. Cedar’s forbid is a natural home.

This split also clarifies ownership. Product and customer-success teams drive relationships (who shared what with whom) through application events that write tuples. Security and compliance teams own the policy repository, reviewed in pull requests, validated in CI against the schema. Neither team blocks the other, and the audit trail is clear about which layer denied a request. If you think about Kubernetes-style admission control, the same idea applies; our piece on Kubernetes ValidatingAdmissionPolicy and CEL policy-as-code without webhooks shows how declarative expression-based policy replaces ad hoc enforcement points, and it shares the “policy as reviewed code” philosophy with Cedar.

Wiring it up: a worked flow

A concrete flow helps. The API gateway receives a request to read asset:pump-17/telemetry and authenticates the caller. It builds a Cedar request: principal User::"alice", action Action::"readTelemetry", resource Asset::"pump-17", with context holding device compliance and request origin. Cedar returns allow or deny. On deny, the gateway returns 403 and logs the determining policy IDs, which Cedar reports in its diagnostics. On allow, the gateway issues a Check to SpiceDB or OpenFGA for asset:pump-17#can_read_telemetry@user:alice. A tuple path, such as Alice to team to site to asset, grants the final allow.

The relationship writes are driven from events. When the facilities system emits “asset pump-17 moved to site B,” a consumer writes a new parent tuple and deletes the old one, in one atomic write request. Both OpenFGA and SpiceDB support batched writes and deletes in a single request, which matters: two separate calls leave a window where the pump belongs to both sites or neither.

Keeping data in sync

The hardest operational problem in any ReBAC deployment is not the engine; it is keeping tuples faithful to the source of truth. If your application database says Alice left the team and the tuple store still says she is a member, you have a security bug with no error message. There are three accepted patterns.

The first is dual write with a reconciler: the application writes to both, and a periodic job diffs and repairs. This is simple, and it is exactly where drift originates, so the reconciler is not optional. The second is the transactional outbox: the application writes its row and an outbox record in a single database transaction, and a relay applies the outbox to the authorization store with idempotent writes. The third is change data capture, where a connector streams row changes into tuple writes. Outbox and CDC both remove the dual-write race; they trade it for a short, bounded lag, which interacts with consistency tokens in the way discussed earlier: store the returned token with the resource once the write lands.

Whichever you choose, make tuple writes idempotent and expose a “full resync for tenant X” operation. You will need it the first time a deploy or an incident corrupts the mapping.

Observability and testing

Instrument decision latency by operation type, cache hit ratio, tuple store read counts per check, and the distribution of check depth. Log every deny with its reason for sampled requests; unexplained denies create support tickets, and unexplained allows create incident reports. Both OpenFGA and SpiceDB expose an Expand-style API to show why access is granted, and you should wire it into an internal “why can this user see this?” debugging tool before your first production incident, not after.

For testing, maintain three suites. Schema assertions list subject, permission, resource, and expected result, run on every change. Property tests generate random graphs and assert invariants, for example “removing a user from every group removes all derived access.” Shadow tests replay sampled production checks against a candidate schema or engine version and report differences. The third suite is what allows you to refactor a schema with confidence.

Trade-offs, Gotchas, and What Goes Wrong

Use the architecture in Figure 1 as the map for the failure modes below.

Stale reads after revocation. The new enemy problem again, but the practical form is subtler. A cache between your app and the authorization store, or a read replica behind it, quietly breaks the guarantees your token strategy depends on. If you adopt SpiceDB’s at-least-as-fresh semantics, make sure no layer in between caches decisions past the token. If you use OpenFGA’s modes, document the staleness window you accept and test that revocation meets it.

Cycles and pathological graphs. A parent relation that points back at a descendant creates a cycle. Engines protect themselves with depth limits and dispatch limits, but the result is an error or a denied check, not a clear explanation. Validate writes in the application so that a folder cannot be moved under its own descendant.

Wildcards and public access. Both Zanzibar descendants support a wildcard subject, meaning everyone. It is also the fastest way to leak a resource: a single stray wildcard tuple on a parent folder makes every descendant public, silently, through inheritance. Restrict who can write wildcard tuples, alert on their creation, and prefer an explicit “public” object over a bare wildcard when auditability matters.

Listing at scale. Reverse lookups are expensive and, on large graphs, can time out or paginate slowly. Treat them as a batch or background capability, and design primary list endpoints around local database filtering plus bulk check, as described earlier. Be sceptical of any demo that lists “all my documents” over a million-node graph with no mention of cost.

Schema as a single point of failure. One typo in a rewrite can broaden access across your whole product. Require review, assertions, and shadow tests, and keep the previous schema version deployable for fast rollback. Both products version their authorization models, so rollback is operationally a pointer change; use it.

Cedar-specific hazards. Because Cedar evaluates only the data you supply, a bug in your entity-building code is a bug in authorization. If you forget to include a user’s group membership, the request is denied; if you include a stale group, it is allowed. The language is verified, but your data plumbing is not. Also remember that forbid beats permit: an overly broad forbid can lock out administrators, so test emergency access paths explicitly.

Operational maturity is uneven. OpenFGA has CNCF incubation, which reflects adoption and governance, not a promise about your scale. SpiceDB’s governance is vendor-led, so the roadmap for advanced scaling features may track the commercial product; read the licence and the release notes for what is open. Cedar’s CNCF membership is at Sandbox level, the earliest maturity stage. None of this disqualifies a tool, but each should enter your risk register.

Vendor-managed options change the calculation. Auth0 FGA (a managed OpenFGA service), AuthZed’s hosted offerings, and Amazon Verified Permissions each move operational burden off you. They also add a network dependency in the hot path of every request, so verify regional availability, latency from your compute, and the failure behaviour of your service when the authorization dependency is unreachable. Decide ahead of time whether you fail closed, which is safer but turns an outage into a total denial of service, or serve from a short-lived decision cache.

Over-modelling. The last anti-pattern is using ReBAC where a column would do. If every resource simply belongs to one tenant and one owner, a tenant ID check in your query is faster, easier to reason about, and has no extra moving parts. Reach for a relationship engine when sharing is user-driven, when hierarchies inherit, or when multiple products need one source of truth for access.

Practical Recommendations

Begin with the question you must answer, not the product. If you need to answer “which resources can this user access” across arbitrary sharing graphs with revocation that cannot lag, start with SpiceDB, or with a managed Zanzibar-style service, and design around its tokens. If you want the quickest adoption for a product team, a straightforward Docker deployment, and can tolerate configurable consistency rather than tokens, start with OpenFGA. If your pain is attribute rules, compliance reviews, and provable guardrails, start with Cedar, and keep your relationship data in your existing database until a graph engine is justified.

Whatever you choose, structure the work in stages. First, model the three or four resource types that account for most of your access checks, and flatten hierarchies to the minimum depth that your product semantics allow. Second, build the tuple synchronisation path before the first production check, because the sync pipeline is the hard part. Third, put the engine behind an internal authorization client library so that application code asks permission questions in your domain vocabulary, and swapping the engine later is a library change rather than a codebase-wide edit.

Treat the engine as a dependency with a service level objective of its own. Track p99 check latency, error rate, and sync lag, and define the fail-closed or fail-open behaviour per endpoint class: fail closed for data exports and admin actions, possibly serve cached decisions for low-risk reads.

A short checklist you can paste into a design review:

  • Define permissions as verbs and relations as nouns; never check raw relations from application code.
  • Cap hierarchy depth, and denormalise tenant membership into a direct tuple.
  • Choose the consistency mode per endpoint, and store tokens with protected resources where the engine supports them.
  • Build tuple sync through an outbox or change data capture, with idempotent writes and a full-resync command.
  • Avoid reverse lookups in hot paths; filter a page locally, then bulk check.
  • Put attribute and compliance rules in Cedar-style policies with a validator and CI analysis.
  • Ban or alert on wildcard tuples on container objects.
  • Maintain assertion tests, property tests, and shadow-evaluation for every schema change.
  • Instrument check latency, cache hits, tuple reads per check, and sync lag.
  • Decide and document fail-closed versus cached-decision behaviour before launch.

The same architectural attitude applies when authorization sits in front of autonomous systems. A control path for a robot fleet or a diffusion-policy-driven manipulator needs the same split between who may command what and what the machine may do in a given context; our overview of diffusion policy for robot manipulation is a useful reminder of how much capability now sits behind those command interfaces.

Frequently Asked Questions

What is relationship-based access control?

Relationship-based access control (ReBAC) decides access from the relationships between subjects and objects, such as “Alice is a member of the platform team, and the platform team can view this folder.” Permissions are computed by traversing a graph of such relationships, with rules for inheritance, for example that folder viewers can view contained documents. Google’s Zanzibar popularised the model, and systems like OpenFGA and SpiceDB implement it as a service.

How does ReBAC differ from RBAC and ABAC?

RBAC grants permissions through roles assigned to users and works well when resources are few and uniform. ABAC evaluates rules over attributes such as department or device state and works well for contextual conditions. ReBAC expresses access through connections between entities, so it handles sharing, nesting, and inheritance naturally. In practice systems blend them: relationships for who, attributes for when and under what conditions.

Is OpenFGA or SpiceDB better?

Neither wins universally. Both implement Zanzibar-style tuples and rewrites. SpiceDB follows the paper’s consistency-token approach most closely and offers multiple datastores and reverse lookup APIs. OpenFGA emphasises simple operation, CNCF governance, and configurable consistency without tokens. Choose by your revocation requirements, operational preferences, and licensing comfort, and run a proof of concept against your own schema and graph shapes before deciding.

Can Cedar replace OpenFGA or SpiceDB?

Usually not. Cedar is a policy language that evaluates the entity data you pass in; it does not store a relationship graph, index it, or serve reverse queries at scale. It can express ReBAC-style rules over small hierarchies you supply. For large, user-driven sharing graphs you still need a tuple store, and Cedar then works well as a guardrail layer in front of it.

What is the new enemy problem?

It is a Zanzibar term for an ordering failure: access is removed from a user, new sensitive content is then added, but a stale authorization snapshot still permits the old user to see it. Zanzibar prevents this with consistency tokens (zookies) that require authorization checks to run at a snapshot at least as fresh as the content change. SpiceDB’s ZedTokens are the open-source analogue.

Do I need a dedicated authorization service at all?

Not always. If access is a tenant ID plus a role, enforcing it in queries and middleware is simpler and faster. A dedicated service pays off when sharing is user-controlled, when permissions inherit through hierarchies, when several services must agree on access decisions, or when auditors require one place to answer who can access what. Introduce it when the cost of scattered logic exceeds the cost of the new dependency.

Further Reading

Disclosure: version and governance facts above were checked against the project pages cited in October 2026. Performance characteristics were not benchmarked by the author, and datastore lists should be confirmed against current documentation before adoption.

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *