EACL is a situated, open-source ReBAC authorization library inspired by SpiceDB.
EACL is built in Clojure and backed by Datomic Pro, Datahike or DataScript.
| Authentication (AuthN) | Authorization (AuthZ) |
|---|---|
Who are you?, i.e. which <subject>? |
What can <subject> do? (what EACL cares about) |
EACL permissions are just data that live next to your Application Data as attributes on entities, hence situated. This has several benefits, mainly reduced latency.
EACL's situated nature makes it suitable for real-time UI view maintenance:
- Let's say you have 1k-10k online clients. When should they refresh their UI? Every time the database changes? It doesn't scale, especially if those queries are expensive.
- EACL makes it cheap & easy to compute which online users are affected by an entity change via
eacl/lookup-subjectsbecause it is designed to compute recursive viewership. - Basically, EACL helps to avoids query amplification without a network hop, while answering authorization questions quickly and correctly.
EACL does not claim to solve the Materialized View problem (related to DDF), but it gets you 95% of the way there, while being fast enough for 1k-10k online users - maybe more.
🦅 EACL is pronounced "EE-kəl", like "eagle" with a k because it keeps a watchful eagle-eye on your permissions.
Is it any good?
Yes. EACL is best-in-class ReBAC authorization for the Clojure ecosystem.
What is EACL good for?
EACL can efficiently answer questions like, "Can <subject> do <permission> on <resource>?" E.g.
(eacl/can? acl (->user "alice") :view (->server "account1") consistency/fully-consistent) => true | false ; 0.01ms-10ms.
Or, "Which <resources> can <subject> do <permission> on?" (as-of <10s ago> or newer)
(eacl/lookup-resources acl {:subject (->user "alice") :permission :view :resource/type :product :first 50 :consistency (consistency/at-least-as-fresh "<10 seconds ago token>")}) => {:data [{:type :product :id "product-1"} {:type :product :id "product-7"} ... {:type :product :id "product-63"}] :page-info ... :cached? true|false ...} ; in ~1-20ms depending on cache, page size & schema complexity.
The :consistency argument is optional. The default is minimize-latency, which means locally-consistent to the Peer.
Refer to the full EACL API.
Overview
- EACL is inspired by SpiceDB, the most faithful open-source implementation of the Google Zanzibar whitepaper.
- Zanzibar powers Google Drive, YouTube, Gmail and Google Calendar, serving billions of authorization requests per day for Billions of (capital B) Relationships.
- EACL is designed as a stepping stone to SpiceDB once you need hyperscale:
- Unlike SpiceDB, EACL is situated, which has several benefits.
- SpiceDB is benchmarked against 100 billion Relationships for
CheckPermission: 5.8ms P95 at 1M QPS with up to 100B relationships on CockroachDB; EACL makes no such scale promises, but if you adopt the ReBAC data model, you will avoid having to rewrite when you do hit hyperscale.
- EACL supports the same consistency semantics as SpiceDB, but owing to EACL's situated nature, some modes are backend-specific, i.e. not every backend supports every mode.
- EACL is fast even without its optional cache.
- For small-to-medium workloads, EACL is faster than Spice (in my experience), but no official benchmarks are published at this time. Under internal testing, EACL performs well against a database with 1M Relationships and complex, real-world recursive schema.
- EACL should comfortably handle 10M-100M Relationships, but I haven't benchmarked 10M Relationships yet. Performance will vary on the complexity of your schema, number of Relationships and lookup query page-size.
- EACL is formally verified using Dafny, TLA+/TLC, and Apalache. In short EACL is IMO, correct:
- The EACL kernel (decision engine + cache) is generated from formal models, i.e. it will never say "yes" when means "no", and it will never serve stale cache segments that are behind time
Tas per your request's consistency constraints. - Clojure/ClojureScript backend implementations are internally certified, but are not generated from proofs.
- EACL does not attempt to verify the correctness of its supported backends – that is the database authors' problem.
- EACL has not been independently audited.
- The EACL kernel (decision engine + cache) is generated from formal models, i.e. it will never say "yes" when means "no", and it will never serve stale cache segments that are behind time
This README is probably too technical – I will simplify it over time with more examples and link to more technical documentation as-needed.
Supported Backends
| Database | Module | Storage |
|---|---|---|
| Datomic Pro | eacl-datomic | DynamoDB (recommended), Cassandra or SQL |
| Datahike | eacl-datahike | DynamoDB, S3 (cheaper, but slower), LMDB, SQL, Redis, GCS or IndexedDB. |
| DataScript | eacl-datascript | In-memory, but can persist to disk or add a SQL adapter. No time-travel. |
Datahike backed by S3 is attractive for infrequently-accessed apps, because you can trade latency for reduced storage cost, and it supports serverless to reduce running cost.
Note: DataScript does not store full history, so it has no at-exact-snapshot semantics. Datahike requires a retained commit graph or temporal history to support exact snapshots.
Coming Soon: Datalevin.
The Benefits of Situated Authorization
EACL's situated philosophy aligns with Datomic: if Data is local and Query is local, perception can scale, so why wait for an external AuthZ system to compute permissions?
As long as the DB basis is recent enough for our consistency demands, we can avoid a network hop. This yields several benefits:
-
Reduced Latency: EACL avoids a network hop to an external AuthZ system, but we can await new data from the Transactor if the Peer is behind time
T, as requested by consistency semantics.Consider that following a mutation, if you want to leverage SpiceDB
at_least_as_freshconsistency semantics to do aLookupResourcesquery, you need to:- Hit the DB or cache for the latest ZedToken pertaining to an entity,
- Pass the ZedToken to SpiceDB so you can retrieve a consistent page of object IDs,
- Hydrate entities from your database using those IDs.
EACL can skip all that stuff because it's all local, man. As long as the Peer has data valid data as-of time
T, so we don't need to wait for anyone, and if we want to make sure, we can useat-least-as-freshorfully_consistentand the Peer will wait until it has the latest data as-ofT, or continue if it has the latest data, via(d/sync conn T).Bonus: Relationships are just data, so permission graph traversal can improve database cache locality for faster entity hydration before display.
Since you have to hit the DB anyway to show anything useful, we might as well compute permissions in the Peer, and that is exactly what EACL does.
-
Time Travel: Unlike Spice cursors, EACL cursors do not expire (and are encrypted for UI exposure) unless you specify a TTL, so we can reconstruct selected snapshots if the backend retains it.
- Note that DataScript does not store full history, so does not support
at-exact-snapshotin the past.
- Note that DataScript does not store full history, so does not support
-
Consistency: Syncing to an external system introduces eventual consistency. With situated AuthZ, queries are at least locally-consistent as-of time
T.- In single-Peer environments, EACL reads from the database currently visible to the local Peer.
- In multi-Peer environments, depending on consistency semantics, EACL may block to catch up to the Transactor if the Peer has fallen behind.
-
Simple Syncing: Relationships are just 3-tuples of
[subject relation resource], so there is no impedance mismatch when syncing to SpiceDB at scale. -
Real-time UI updates for materialized views: it is cheap to compute the subset of online clients that need to re-query while avoiding query amplification due to a busy Transactor.
-
Situated is faster for small (~1k-100k relationships) to medium-applications (~1M-10M Relationships):
- Authorization runs in-process, so entities can be hydrate without a network hop.
- End-to-end queries don't need to block on network I/O when data is local to the Peer.
-
Application & Authorization Data live together in harmony. In my testing with small to medium-sized workloads, EACL is as good, or faster than SpiceDB, owing to reduced latency from its situated design, but no EACL benchmarks are published at this time (benchmarks are a tricky business).
-
Planning for Scale: Avoid a rewrite later by getting your ReBAC data model right the first time.
-
EACL has limitations compared to SpiceDB: mainly, no Caveats (yet), Negation or Intersection operators (yet), and a few other minor differences.
-
One less external dependency to deploy & sync relationships to.
ReBAC: Relationship-based Access Control
In a ReBAC system like EACL, objects (Subjects & Resources) are related via Relationships.
A Relationship is just a 3-tuple of [subject relation resource], e.g.
[user1 :owner account1]means subjectuser1is the:ownerof resourceaccount1, and[account1 :account product1]means subjectaccount1is the:accountfor resourceproduct1.
EACL models two core concepts to model the permission graph: Schema & Relationship.
- Schema consists of
RelationsandPermissions:Relationdefines how a<subject>&<resource>can be related via aRelationship.Permissiondefines which permissions are granted to a subject via a chain ofRelationshipsbetween subjects & resources.- Permissions can be Direct Permissions or indirect, known as Arrow Permissions. An arrow implies a graph traversal.
- A Relationship defines how a
<subject>and<resource>are related via a named relation, e.g.[(->user alice) :owner (->account "acme")]means that(->user "alice")is the Subject,:owneris the name of theRelation(as defined in the schema)(->account "acme")is the Resource- so this reads as
(->user "alice")is the:ownerof(->account "acme"). - In EACL, this is expressed as
(->Relationship (->user "alice") :owner (->account "acme")), i.e.(Relationship subject relation resource) - Subjects & Resources are just maps of
{:keys [type id]}, e.g.{:type :user, :id "user-1"}, or(->user "user-1")when using a helper function.
Data Structures
Relationships
EACL Relationships are light. Relationships are stored directly on entities as two tuples:
- Forward subject->resource tuple:
:eacl.v7.relationship/subject-type+relation+resource-type+resource - Reverse resource->subject tuple:
:eacl.v7.relationship/resource-type+relation+subject-type+subject
To retract Relationships, install & use :eacl.fn/retractEntity or call eacl/delete-relationships! – not :db.fn/retractEntity, or you will leave ghost Relationship tuples lying around. EACL's contract with you is that you need to use EACL's API to maintain Relationships to guarantee cache coherence and a clean database. If you mess with EACL's data structures, it becomes your problem.
But you will probably forget, so there are helpers to clean up ghost tuples. Refer Deleting a permissioned entity.
Relations:
:eacl.relation/resource-type:eacl.relation/relation-name:eacl.relation/subject-type:eacl.relation/resource-type+relation-name+subject-type:eacl/relation-versionis needed for cache coherence.
Permissions
:eacl.permission/resource-type:eacl.permission/permission-name:eacl.permission/source-relation-name:eacl.permission/target-type:eacl.permission/target-name
Permission indices (tuples):
- Direct Permissions:
:eacl.permission/resource-type+permission-name - Datomic permission arrows:
:eacl.permission/resource-type+source-relation-name+target-type+permission-name - Datomic relation arrows:
:eacl.permission/resource-type+source-relation-name+target-type+target-name - Datomic full key:
:eacl.permission/resource-type+source-relation-name+target-type+target-name+permission-name - Datahike and DataScript full key:
:eacl.permission/full-key
EACL Schema
Internal EACL data:
:eacl/iduniquely identifies Relations & Permissions. It's a string to match SpiceDB IDs.:eacl/schema-stringstores the valid schema string that was written viaeacl/write-schema!.:eacl/schema-versionis Datomic's schema generation.:eacl/schema-generationand:eacl/schema-write-fencetrack schema writes in Datahike and DataScript.:eacl/storage-versionidentifies Datomic's current Relationship storage model as version 7.:eacl.fn/assert-relation-unusedis Datomic's commit-time guard against removing a Relation that still has Relationships.
Consistency Semantics
EACL uses the same four consistency-mode names as SpiceDB, but each adapter advertises only the guarantees its backend can provide:
minimize-latency(the default) selects the current immutable database value visible to the local backend connection without an explicit synchronization barrier.at-least-as-freshselects a database value at least as new as an authenticated EACL mutation token, or waits up to the configured synchronization timeout when the backend can catch up.at-exact-snapshotselects the exact database value named by a token. Datomic supports this while history is available. Datahike supports it when a retained commit graph or temporal history can reconstruct the value. DataScript rejects it.fully-consistentrequests the backend's authoritative-head barrier. For Datomic this synchronizes the Peer before selecting a DB. Datahike advertises it only with a direct:selfwriter. A connection-backed DataScript client serializes selection of the current connection head.
Unsupported modes fail with a typed error instead of silently selecting a weaker mode. EACL's fully-consistent is defined by the configured backend's synchronization boundary; it is not a claim about an external globally distributed system. See Consistency and Zed tokens for usage.
Performance
- EACL results are stable-discovery ordered. Reachable permission schema from the queried root is compiled into a sealed plan (dense canonical rule ordinals plus a certified static read-cost rank), and a single width-one depth-first reducer walks that plan over ordered backend index scans (
seek-datomson the relationship endpoint tuples), admitting each (node, entity) exactly once. It avoids both recursive Datalog materialization and persisted grant caches. Lookup results are returned in the plan's stable first-discovery order — deterministic for one immutable snapshot, schema and query, but not a global entity-ID sort. Refer to docs/stable-discovery-engine.md. - EACL is fast, but makes no strong performance claims at this time. For typical workloads, EACL should be as fast as, or faster than, SpiceDB. EACL is not meant for hyperscalers.
- EACL is internally benchmarked against ~800k permissioned resources with good latency (5-30ms per query). You can scale Datomic Peers horizontally and dedicate peers to EACL as needed.
- The performance goal for EACL is to handle 10M permissioned entities with real-time performance.
- EACL should be good for small (~10k-100k Relationships) to medium-scale (250k-1M Relationships). You can scale Peers horizontally and may never need to migrate from EACL to SpiceDB.
- EACL does not support all SpiceDB features. Please refer to the limitations section to decide if EACL is right for you.
- EACL uses a bounded, client-private cache. Repeated operations reuse complete answers at the same immutable snapshot (and, when the proof-backed dependency check passes, across unrelated transactions); continued pages reuse the latest engine checkpoint for their exact snapshot; sealed plans are cached per source and basis. The cache never changes authorization semantics and can be disabled globally or per request. See Caching.
- Lookup cursors are result edges (the boundary result's one-based ordinal and identity, bound to the sealed plan's fingerprint) carried inside an authenticated envelope. A continued page resumes from the client-private latest checkpoint for that exact snapshot when one is retained, and otherwise replays the authenticated prefix deterministically against the same snapshot before publishing anything.
- A first page costs the reader roughly the index scans on the cheapest certified path to the first results (the reducer follows the lowest static read-cost alternatives first), not the realization of every union branch. Continuation hits make a sequential walk approximately linear in traversed work; a continuation miss deterministically replays the prefix against the same exact snapshot. Counts exhaust the same reducer and read its scalar discovered count; pass
:count-limitto bound that work. Subjects are typically sparse compared to resources, i.e. 1k users will have access to 1M resources – rarely the other way around.
Public cursors are opaque, authenticated, and tied to the query and database
snapshot that created them. A cursor walk stays on that snapshot even when the
current database advances. Cursors have no age expiry unless
:cursor-ttl-seconds is configured. If a conditionally historical backend can
no longer reconstruct the selected value, EACL returns a typed
snapshot-unavailable error; ordinary Datomic history and history-enabled
Datahike do not expire a cursor merely because it is old.
Project Status
Warning
EACL is under active development.
I try hard not to introduce breaking changes, but if data structures change, the major version will increment.
The current version is the EACL 8.0 release candidate.
The four 8.0.0-SNAPSHOT artifacts are available from Clojars under the
verified dev.eacl group.
Modules
Choose the adapter for your backend. It brings in the shared EACL module at the same version:
;; Datomic Pro {:deps {dev.eacl/eacl-datomic {:mvn/version "8.0.0-SNAPSHOT"}}} ;; Datahike {:deps {dev.eacl/eacl-datahike {:mvn/version "8.0.0-SNAPSHOT"}}} ;; DataScript {:deps {dev.eacl/eacl-datascript {:mvn/version "8.0.0-SNAPSHOT"}}} ;; Core-only consumers and backend authors {:deps {dev.eacl/eacl {:mvn/version "8.0.0-SNAPSHOT"}}}
For source development, clone the full repository, prepare the generated core
runtime as described below, then keep the same library coordinate and use
:local/root. The backend module resolves the sibling core module:
{:deps {dev.eacl/eacl-datomic
{:local/root "/absolute/path/to/eacl/core/modules/eacl-datomic"}}}Development from source
Source consumers who compile the EACL kernel locally need the Clojure CLI,
Node.js, and the repository-pinned Dafny, Apalache, and TLA+ tools. Prepare the
generated JVM and browser runtimes before using a :local/root dependency:
cd modules/eacl # Default Java target clojure -T:build prep # Example Java 17 target clojure -T:build prep :java-release 17 clojure -T:build jar :java-release 17
Pass the same :java-release to prep and jar or install. The default is
Java 26; source builds may target Java 8 through Java 26, subject to their
backend and application dependencies. See formal/README.md
for tool versions and the full verification commands.
EACL does not select a logging implementation. Applications remain responsible for their own logging backend and configuration.
For module selection, current capability differences, cache mutation rules, and recursive controls, see the backend guide. Backend authors should also read the adapter boundary.
Schema & Relationships
To create a Relationship, first define your schema using eacl/write-schema!:
(eacl/write-schema! acl "definition user {} definition account { relation owner: user relation viewer: user permission admin = owner } definition product { relation account: account permission edit = account->admin permission view = account->admin + account->viewer }")
This schema defines:
- An
accountcan haveownerandviewerusers, withadminpermission granted to owners - A
productbelongs to anaccount, witheditpermission for account admins andviewpermission for account admins and viewers
In SpiceDB schema DSL, + means union (OR-logic). EACL does not support exclusion (-) or intersection (&) yet.
EACL API
The IAuthorization protocol in modules/eacl/src/eacl/core.cljc defines an idiomatic Clojure interface that maps to and extends the SpiceDB gRPC API:
Permission Checks
(eacl/can? acl subject permission resource) => true | false
(eacl/check-permission acl {:subject subject, :permission permission, :resource resource}) => {:allowed? true, :cached? boolean, :cache-basis ...}
Lookups
(eacl/lookup-resources acl filters) => {:data [resources...] :page-info {...} :cached? boolean :cache-basis ...}
(eacl/lookup-subjects acl filters) => {:data [subjects...] :page-info {...} :cached? boolean :cache-basis ...}
Counting
(eacl/count-resources acl filters) => {:count 42, :limit -1, :cached? boolean, :cache-basis ...}
(eacl/count-subjects acl filters) => {:count 7, :limit -1, :cached? boolean, :cache-basis ...}
Without :count-limit, :limit is -1 and the count operation exhausts the
result set. Pass :count-limit n to bound work. The result then includes
:truncated?; true means at least one additional result exists.
Relationship Maintenance
(eacl/read-relationships acl filters) => {:data [relationships...] :page-info {...} :cached? boolean :cache-basis ...}(eacl/write-relationships! acl updates) => {:zed/token "eacl_z4_..."},- where
updatesis a collection ofRelationshipUpdaterecords ((eacl/->RelationshipUpdate operation relationship)) or maps{:operation op :relationship rel}, andoperationis one of:create,:touchor:delete. A bare[operation relationship]vector is rejected as an unsupported update. - schema names are validated before any endpoint is resolved: an unknown definition, relation, or a subject type the relation does not declare fails with the same typed
:eacl/unknown-definition/:eacl/unknown-relation-or-permissionerrors the read operations use. :createfails with:eacl/relationship-conflictwhen the relationship already exists, and the check is decided inside the transaction on every backend (Datomic: a transactor-side relation stamp CAS with re-planning; DataScript and Datahike with the default in-process writer: a transaction function), so two racing:creates of one relationship produce exactly one success. A Datahike remote writer cannot transport a transaction function and keeps the plan-time check only.:touchis idempotent. Repeating one operation for the same relationship inside a batch has the same outcome as submitting it once (:createstill conflicts when the relationship existed before the batch); mixing different operations for the same resolved relationship throws:eacl/invalid-relationship-update-batchbefore submission.
- where
(eacl/create-relationships! acl relationships)simply callswrite-relationships!with:createoperation.(eacl/delete-relationships! acl relationships)simply callswrite-relationships!with:deleteoperation.(eacl/delete-object! acl object) => {:zed/token "eacl_z4_...", :retracted-datoms n}is a convenience helper that removes every relationship touchingobject, in both directions.ncounts relationship datoms actually retracted by the committed transactions. On Datomic the retractions are committed in batches of 1,000 (a concurrent reader can observe a partially deleted object between batches); on DataScript and Datahike they are one atomic transaction. Consumers are expected to delete relationships before retracting a permissioned entity — see Deleting a permissioned entity.
All list APIs use the v8 Relay pagination contract:
- Forward: pass
:firstand optionally:after. - Backward: pass
:lastand optionally:before. - Responses include
:page-infowith:start-cursor,:end-cursor,:has-next-page?, and:has-previous-page?. - Lookup cursors paginate in the sealed plan's stable first-discovery order; a page size change is rejected as an incompatible cursor rather than silently re-windowed.
Deadlines and cooperative cancellation
Every bounded read accepts an optional per-request :cancellation-token in
addition to :timeout-ms. Create and cancel the token through the public EACL
API:
(let [token (eacl/cancellation-token)] ;; Pass `token` to the HTTP/request owner before starting the read. (future (eacl/lookup-resources acl {:subject (eacl/spice-object :user "alice") :permission :view :resource/type :document :first 100 :cancellation-token token})) (eacl/cancel! token))
Cancellation is cooperative and best-effort. EACL checks it at the same
orchestration, cursor, cache, and reducer-transition boundaries (one check per
engine step, which covers each adapter command) as the absolute deadline and, when observed before completion, throws
:eacl.execution/cancelled without returning a partial answer. A synchronous
adapter call already in progress must return before the next check, and a
completed result may win a race with a late cancellation. Applications must
therefore keep the server deadline and bounded admission control; interrupting
a worker thread is not a substitute. The token is execution-only and is
excluded from cache, continuation, and authenticated cursor identity. One
token belongs to one logical request.
Schema Maintenance
(eacl/write-schema! acl schema-string)parses a SpiceDB schema DSL string, validates it, computes deltas against existing schema, checks for orphaned relationships, and transacts changes atomically.(eacl/read-schema acl)returns the current schema as a map of{:relations [...] :permissions [...]}.
All schema changes must use eacl/write-schema!. If an application changes
the authorization schema directly, follow the recovery procedure in
Caching before resuming authorization traffic.
Permission-tree expansion
Expansion accepts exactly :resource, :permission, and the optional
:consistency, :timeout-ms, and :cancellation-token keys:
(eacl/expand-permission-tree acl {:resource (eacl/spice-object :document "readme") :permission :view :consistency consistency/fully-consistent :timeout-ms 5000}) ;; => ;; {:expanded-at "eacl_z4_..." ;; :tree-root ;; {:expanded-object {:type :document :id "readme"} ;; :expanded-relation :view ;; :intermediate ;; {:operation :union ;; :children ;; [{:expanded-object {:type :document :id "readme"} ;; :expanded-relation :viewer ;; :leaf {:subjects [{:type :user :id "alice"}]}}]}}}
A node contains exactly one of :leaf or :intermediate. Permission and
arrow boundaries remain visible; expansion is shallow in the SpiceDB sense,
so leaves contain subjects found by direct relation scans rather than a
flattened effective-membership set. To decide whether a subject has the
permission, use can?; do not infer authorization by flattening a tree.
Child and subject vector order is non-semantic and may differ by backend. Empty branches and duplicate paths are preserved. Compare trees as annotated topology with child/subject multisets when order is irrelevant. The exact supplied root ID is retained, while scanned IDs are converted with the selected client's object-ID codec.
The response tree and :expanded-at token are derived from the same selected
immutable snapshot. Replay the token with
(consistency/at-exact-snapshot (:expanded-at response)) only on a backend
that advertises exact historical selection; otherwise use it as an
at-least-as-fresh causal floor. Unsupported consistency, unavailable history,
deadlines, unknown root relations or permissions, cycles, codec failures,
adapter-contract failures,
and structural limits produce typed all-or-error failures—no lazy or partial
tree is returned.
Clients accept positive exact-integer :permission-tree-limits overrides.
They are configuration-only, not request keys:
(eacl.datascript.core/make-client conn {:permission-tree-limits {:max-depth 50 :max-schema-components 100000 :max-relationship-values 100000 :max-tree-nodes 100000 :max-leaf-subjects 100000}})
Every bundled backend uses the same portable expansion kernel. Expected backend differences include supported consistency modes, historical retention, native scan order, and configured identity conversion.
Example Queries
The primary API call is can?, e.g.
(eacl/can? acl subject permission resource) => true | false
The other primary API call is lookup-resources, e.g.
(def page1 (eacl/lookup-resources acl {:subject (->user "alice") :permission :view :resource/type :server :first 2})) ; defaults to 1000. page1 => {:data [{:type :server :id "server-1"} {:type :server :id "server-2"}] :page-info {:start-cursor "..." :end-cursor "..." :has-next-page? true :has-previous-page? false} :cached? boolean :cache-basis ...}
To query the next page, pass the :end-cursor from page1 as :after:
(def page2 (eacl/lookup-resources acl {:subject (->user "alice") :permission :view :resource/type :server :first 2 :after (get-in page1 [:page-info :end-cursor])})) page2 => {:data [{:type :server :id "server-3"} {:type :server :id "server-4"}] :page-info {:start-cursor "..." :end-cursor "..." :has-next-page? true :has-previous-page? true} :cached? boolean :cache-basis ...}
To go back from page2, pass its :start-cursor as :before with :last:
(eacl/lookup-resources acl {:subject (->user "alice") :permission :view :resource/type :server :last 2 :before (get-in page2 [:page-info :start-cursor])})
Forward and backward pages return results in the same order for one fixed query and authenticated cursor walk. Permission lookups use the sealed plan's stable first-discovery order, and relationship reads use backend tuple-index order. These are pagination orders, not a global, cross-backend, or domain sort order. Backward pagination returns the previous window; it does not reverse the result order.
Quickstart
Datomic Pro
Add the Datomic adapter dependency to your deps.edn file:
{:deps {dev.eacl/eacl-datomic {:mvn/version "8.0.0-SNAPSHOT"}}}(ns my-eacl-project (:require [datomic.api :as d] [eacl.core :as eacl :refer [->Relationship spice-object]] [eacl.datomic.core] [eacl.datomic.schema :as schema])) ; Create an in-memory Datomic database: (def datomic-uri "datomic:mem://eacl") (d/create-database datomic-uri) ; Connect to it: (def conn (d/connect datomic-uri)) ; Install EACL's current Datomic Relationship schema: @(d/transact conn schema/v7-schema) ; Make an EACL client that satisfies the `IAuthorization` protocol: (def acl (eacl.datomic.core/make-client conn {:object-id->lookup-ref (fn [obj-id] [:eacl/id obj-id]) :entid->object-id (fn [db eid] (:eacl/id (d/entity db eid)))})) ; Write your permission schema using SpiceDB schema DSL: (eacl/write-schema! acl "definition user {} definition account { relation owner: user permission admin = owner permission update = admin } definition product { relation account: account permission edit = account->admin }") ; Transact some Datomic entities with a unique ID, e.g. `:eacl/id`: @(d/transact conn [{:eacl/id "user-1"} {:eacl/id "user-2"} {:eacl/id "account-1"} {:eacl/id "product-1"} {:eacl/id "product-2"}]) ; Define some convenience methods over spice-object: ; `eacl.core/spice-object` constructs a SpiceObject from `type`, `id`, and an ; optional subject relation. EACL queries do not support subject relations. (def ->user (partial spice-object :user)) (def ->account (partial spice-object :account)) (def ->product (partial spice-object :product)) ; Write some Relationships to EACL. For same-transaction entity and ; Relationship creation, use the explicit tx-relationship example below: (eacl/create-relationships! acl [(eacl/->Relationship (->user "user-1") :owner (->account "account-1")) (eacl/->Relationship (->account "account-1") :account (->product "product-1"))]) ; Run some Permission Checks with `can?`: (eacl/can? acl (->user "user-1") :update (->account "account-1")) ; => true (eacl/can? acl (->user "user-2") :update (->account "account-1")) ; => false (eacl/can? acl (->user "user-1") :edit (->product "product-1")) ; => true (eacl/can? acl (->user "user-2") :edit (->product "product-1")) ; => false ; You can enumerate the :product resources a :user subject can :edit via `lookup-resources`: (eacl/lookup-resources acl {:subject (->user "user-1") :permission :edit :resource/type :product :first 1000}) ; => {:data [{:type :product, :id "product-1"}] ; :page-info {:start-cursor "eacl4_..." ; :end-cursor "eacl4_..." ; :has-next-page? false ; :has-previous-page? false} ; :cached? false ; :cache-basis ...}
Datahike Quickstart
For Clojure/JVM applications backed by Datahike, add the Datahike adapter
dependency to your deps.edn file:
{:deps {dev.eacl/eacl-datahike {:mvn/version "8.0.0-SNAPSHOT"}}}(ns my-eacl-datahike-project (:require [datahike.api :as d] [eacl.core :as eacl] [eacl.datahike.core :as eacl.datahike])) ; Create an in-memory Datahike database and install EACL's Datahike schema: (def conn (eacl.datahike/create-conn)) ; Make an EACL client that satisfies the `IAuthorization` protocol: (def acl (eacl.datahike/make-client conn {})) ; Write your permission schema using SpiceDB schema DSL: (eacl/write-schema! acl "definition user {} definition account { relation owner: user permission admin = owner }") ; Transact application entities with unique `:eacl/id` values: (d/transact conn [{:eacl/id "user-1"} {:eacl/id "account-1"}]) ; Create a Relationship between existing entities: (eacl/create-relationship! acl (eacl/spice-object :user "user-1") :owner (eacl/spice-object :account "account-1")) ; Run a Permission Check with `can?`: (eacl/can? acl (eacl/spice-object :user "user-1") :admin (eacl/spice-object :account "account-1")) ; => true
EACL-created Datahike databases enable :keep-history? true by default so
exact tokens and cursors survive ordinary commit-record cutoff collection.
Pass {:keep-history? false} to create-conn only when lower write/storage
amplification is worth making exact reconstruction conditional on retained
commit records.
DataScript Quickstart
For server-side or browser demos, use the DataScript adapter:
{:deps {dev.eacl/eacl-datascript {:mvn/version "8.0.0-SNAPSHOT"}}}(ns my-eacl-datascript-demo (:require [datascript.core :as ds] [eacl.core :as eacl] [eacl.datascript.core :as eacl.datascript])) (def conn (eacl.datascript/create-conn)) (def acl (eacl.datascript/make-client conn {})) (ds/transact! conn [{:db/id -1 :eacl/id "user-1"} {:db/id -2 :eacl/id "account-1"}]) (eacl/write-schema! acl "definition user {} definition account { relation owner: user permission admin = owner }") (eacl/create-relationship! acl (eacl/spice-object :user "user-1") :owner (eacl/spice-object :account "account-1")) (eacl/can? acl (eacl/spice-object :user "user-1") :admin (eacl/spice-object :account "account-1")) ; => true
EACL Schema
EACL parses a documented subset of the SpiceDB schema DSL to define your authorization model. Use eacl/write-schema! to parse, validate, and transact your schema:
(eacl/write-schema! acl "definition user {} definition account { relation owner: user permission admin = owner permission update = admin } definition product { relation account: account permission edit = account->admin }")
Schema Validation
write-schema! validates your schema and provides informative error messages. An invalid schema throws and nothing is transacted:
- Parse validation: unparseable schema strings and duplicate
definition/relation declarations throw.//and/* */comments are supported. - Reference validation: all relations and permissions must reference valid definitions. Arrow targets must exist on every subject type of the source relation.
- Orphan protection: relations with existing relationships cannot be deleted.
- Empty-schema guard: the public
eacl/write-schema!rejects replacing a non-empty schema with zero definitions. The backend schema namespaces expose a lower-level{:allow-empty-schema? true}option for an intentional wipe; direct use must also follow the cache-recovery rules because it bypasses the EACL client. - Unsupported feature detection: rejects SpiceDB features unsupported by EACL (see Limitations)
Schema Updates
When you call write-schema! with a modified schema, EACL:
- Parses the new schema
- Computes deltas (additions/retractions) against existing schema
- Validates retractions won't orphan existing relationships
- Transacts changes atomically
Modelling Relations
Let's model the following SpiceDB schema in EACL:
definition user {}
definition account {
relation owner: user
}
We define two resource types, user & account, where any user subject can be the :owner of an account resource.
A Relationship is just a 3-tuple of [subject relation resource]:
(eacl/->Relationship (->user "alice") :owner (->account "acme"))
Permission Schema: Direct Relations
Let's add a direct permission to the schema for account resources:
(eacl/write-schema! acl "definition user {} definition account { relation owner: user permission update = owner }")
Here, permission update = owner means any user who is an :owner of an account will have the update permission for that account.
At this point, all permissions checks via eacl/can? will return false, because there are no Relationships defined:
(eacl/can? acl (->user "alice") :update (->account "acme")) => false
What happens when we create some Relationships between users & accounts?
Creating Relationships
In EACL, Relationships are expressed as 3-tuples of [subject relation resource] using the ->Relationship helper, e.g. user alice is an :owner of acme account:
(eacl/->Relationship (->user "alice") :owner (->account "acme"))
Now let's create a Relationship between a user subject and an account resource using eacl/create-relationships!:
(eacl/create-relationships! acl [(eacl/->Relationship (->user "alice") :owner (->account "acme"))])
Note: eacl/create-relationships! is just a wrapper over eacl/write-relationships! with the :create operation. It will throw if there is an existing relationship that matches input.
Permission Checks
Now that we have created a Relationship between a user and an account, we call eacl/can? to check if a user has the :update permission on the ACME account, e.g. "can Alice :update the ACME account?"
(eacl/can? acl (->user "alice") :update (->account "acme")) => true
Indeed, she can. Why? Because Alice is an :owner of the ACME account and the :update permission is granted to all users who are :owner(s).
Can Bob :update the ACME account?
(eacl/can? acl (->user "bob") :update (->account "acme")) => false
No, he cannot, because Bob is not an :owner of the ACME account.
Arrow Permissions
Arrow permissions imply a graph hop. Arrows are designated by -> in the SpiceDB schema DSL:
(eacl/write-schema! acl "definition user {} definition account { relation owner: user permission admin = owner permission update = admin } definition product { relation account: account permission edit = account->admin }")
Here, permission edit = account->admin states that subjects are granted the edit permission if, and only if they have the admin permission on the related account for that product. Only account owners have the admin permission on the related account. So given that:
(->user "alice")is the:ownerof(->account "acme"), and(->account "acme")is the:accountfor(->product "SKU-123"),- EACL can traverse the permission graph from user -> account -> product to derive that Alice has the
:editpermission on productSKU-123.
Now you can use can? to check those arrow permissions:
(eacl/can? acl (->user "alice") :edit (->product "SKU-123")) => true ; if Alice is an :owner of the Account for that Product. (eacl/can? acl (->user "bob") :edit (->product "SKU-123")) => false ; if Bob is not the :owner of the Account for that Product.
Internally, EACL stores relation and permission definitions as entities and stores each relationship in both directions for efficient traversal.
EACL ID Configuration
SpiceDB uses strings for external subject and resource IDs, whereas the bundled EACL adapters traverse backend-native entity IDs internally. EACL lets you configure how internal IDs are converted to external IDs and vice versa.
Note: internal Datomic eids should not be exposed to consumers, because those eids are not guaranteed to be stable after a DB rebuild.
Every bundled adapter's make-client accepts
:entid->object-id/:object-id->lookup-ref functions for converting between
internal entity IDs and external object IDs. The following example uses Datomic.
It is common to attach a unique UUID to permissioned entities for exposing them externally, or you can convert external->internal at your call sites. Here is how you can configure EACL to convert to/from a unique attribute named :your/id:
(def acl (eacl.datomic.core/make-client conn {:entid->object-id (fn [db eid] (:your/id (d/entity db eid))) :object-id->lookup-ref (fn [obj-id] [:your/id obj-id])}))
Note that this attribute should have property :db/unique :db.unique/identity.
The default options are to use the built-in EACL string attr :eacl/id, but you can use the internal Datomic eids with the following "identity" functions:
(def acl (eacl.datomic.core/make-client conn {:entid->object-id (fn [_db eid] eid) :object-id->lookup-ref (fn [obj-id] obj-id)}))
make-client rejects unknown options with {:type :eacl/invalid-config}.
All backends issue non-expiring cursors by default. Configure a positive
:cursor-ttl-seconds only when the application deliberately wants a maximum
pagination age; cache TTL and capacity remain independent of cursor age.
Caching
Caching is automatic, bounded, and private to each EACL client. Cache data is
never written to the application database. EACL first looks for an answer from
the exact immutable database value selected by the request. Authenticated
at-exact-snapshot requests may reuse a completed answer only when the full
source/lifecycle, native locator, ordinary-view, adapter/identity, engine,
request, result-shape, demand, and limit identity matches. Exact requests never
use managed proof-backed lifting. Ordinary current requests may reuse an older
answer only when EACL establishes that the relevant schema and relationships
have not changed. If it cannot establish either condition safely, it runs the
authorization query normally.
A long-running request can continue using the immutable database value it
started with while newer requests see newer data. EACL does not promise cache reuse for arbitrary as-of, since, filtered, speculative, or caller-constructed database values.
Cache coherence is only guaranteed as long as authorization mutations use EACL's supported APIs:
- Change schema with
eacl/write-schema!. - Add/retract relationships via EACL relationship APIs
- Retract permissioned entities via :eacl.fn/retractEntity.
Ordinary application datoms that do not affect authorization are unrestricted. If an application changes EACL schema or relationship storage directly, splits EACL transaction data, changes the identity of a permissioned object outside the documented contract, or leaves relationships behind during deletion, cached authorization results may be stale.
To recover after an unsupported authorization mutation:
- Stop affected authorization traffic in every process.
- Repair the schema, identity, or relationship data through a supported EACL path.
- Expire or recreate every affected EACL client in every process.
- Resume traffic only after repair and cache rotation are complete.
Cache expiry removes remembered answers; it does not repair ghost relationships. Rewriting an unchanged schema is not a cache flush.
Most applications need no cache configuration. Disable caching for one client with eacl.cache/no-cache:
(require '[eacl.cache :as eacl-cache]) (def acl (eacl.datomic.core/make-client conn {:cache eacl-cache/no-cache}))
Or bypass the cache for one request:
(eacl/can? acl {:subject alice :permission :view :resource doc :cache? false})
Use eacl/check-permission when a caller needs cache provenance in addition
to the Boolean decision:
(eacl/check-permission acl {:subject alice :permission :view :resource doc}) ;; => {:allowed? true, :cached? false, :cache-basis ...}
Inspect or expire a client through its backend API:
(eacl.datomic.core/cache-stats acl) (eacl.datomic.core/expire-cache! acl) (eacl.datahike.core/cache-stats acl) (eacl.datahike.core/expire-cache! acl) (eacl.datascript.core/cache-stats acl) (eacl.datascript.core/expire-cache! acl)
After a database restore, reset, branch replacement, or other operation that can replace history, expire or replace every affected client before serving requests. Multi-process deployments that exchange cursors or tokens must coordinate the source-lifecycle rotation described in the cache guide.
Custom ID converters remain local to one client unless every participating process uses the same deterministic converter and stable adapter fingerprint.
For cache tuning, custom identity codecs, metrics, proof availability, and the full recovery and correctness model, read Cache behavior and coherence.
Consistency and Zed tokens
Authorization defaults to the immutable database value currently visible to the local backend. Mutation responses include an authenticated revision token. Reads can request stronger behavior when the backend supports it:
(require '[eacl.spicedb.consistency :as consistency]) ;; Default: current local database value. (eacl/can? acl subject :view resource consistency/minimize-latency) ;; Synchronize before selecting the database value. (eacl/can? acl subject :view resource consistency/fully-consistent) ;; Read at least as new as an earlier EACL mutation. (eacl/can? acl subject :view resource (consistency/at-least-as-fresh write-token)) ;; Read the exact historical snapshot named by a token, if available. (eacl/can? acl subject :view resource (consistency/at-exact-snapshot prior-token))
Datomic exact selection treats an authentic same-source token ahead of the
local Peer as replica lag: it performs bounded (d/sync conn T) when needed,
verifies the returned basis, and always evaluates (d/as-of db T). A locally
available T skips synchronization. Ordinary unreplaced Datomic history has
no EACL cursor-retention window.
EACL-created Datahike databases retain temporal history by default. External history-enabled Datahike stores can reconstruct exact revisions after commit record collection; history-disabled stores advertise only conditional exact selection while a named commit is retained. DataScript does not provide general historical snapshot reconstruction. If a backend cannot satisfy the requested guarantee, EACL returns a typed error rather than silently selecting a different snapshot.
Treat Zed tokens as opaque. A token proves freshness only for its original
backend, database, branch, and lifecycle. For a token returned through an
untrusted frontend, the backend should normally choose
at-least-as-fresh. Do not let a frontend request exact historical
authorization without a separate authorization decision.
Multi-process deployments must configure the same cursor and Zed-token verification keys on every instance that accepts the same tokens:
(def acl (eacl.datomic.core/make-client conn {:security-key "32+ bytes of shared secret key material" :security-kid :cursor-2026-07 :zed-token-keyring {:zed-2026-06 old-zed-root :zed-2026-07 current-zed-root} :zed-token-kid :zed-2026-07}))
Retain old verification keys for the intended token lifetime during key rotation. The default keys are client-local, so default cursors and tokens do not survive restarts or load balancing.
See the backend guide for exact capabilities, synchronization timeouts, checkpoints, key rotation, and recursive traversal controls.
Unknown object IDs
EACL's bundled situated backends require object IDs to resolve to application entities:
- Reads (
can?,lookup-resources,lookup-subjects,count-resources,count-subjects,read-relationships) treat unknown IDs as matching nothing:can?returnsfalse, lookups and reads return empty pages. - Writes (
write-relationships!and friends) throwex-info {:type :eacl/unknown-object, :object {:type … :id …}}— a relationship to a nonexistent entity is unsatisfiable, and failing loudly beats minting ghost entities or raw Datomic errors.
If a lookup result has no external ID in the selected database,
lookup-resources and lookup-subjects raise
{:type :eacl/unresolvable-object} and identify every offending internal ID
instead of silently omitting authorized objects. This usually indicates a
dangling relationship left by retracting an entity before its relationships.
read-relationships still returns the damaged relationship half with a nil
ID so it can be repaired.
Deleting a permissioned entity
Important
Do not call the backend's ordinary entity-retraction operation on a permissioned entity before removing its EACL relationships.
EACL stores both directions of a relationship. A native entity retraction removes the half stored on the target, but it cannot follow the peer ID stored inside the other endpoint's tuple or vector. The surviving half is a ghost relationship and can continue granting access.
The portable deletion sequence is:
;; Remove every relationship touching the object in both directions. (eacl/delete-object! acl (->account "acme")) ;; Then delete the application entity with the backend's normal operation. @(d/transact conn [[:db.fn/retractEntity account-eid]])
delete-object! removes relationships but does not delete the application
entity. It is idempotent. The Datomic implementation batches high-degree
cleanup; the Datahike and DataScript implementations use one transaction.
Backends that support transaction functions also provide an optional atomic
:eacl.fn/retractEntity. It removes both relationship halves and the target
entity in one transaction. The function is not installed by the normal EACL
schema; enabling it is an explicit deployment step.
| Backend/configuration | Safe-retraction support |
|---|---|
| Datomic Peer/Pro | Named :eacl.fn/retractEntity |
| DataScript CLJ/CLJS | Named or direct in-process function |
| Datahike with an in-process writer | Named or direct, depending on schema configuration |
| Datahike remote/function-unsafe writer | Use delete-object! and ordinary deletion |
Datomic example:
(require '[datomic.api :as d] '[eacl.datomic.safe-retraction :as safe-retraction]) ;; Privileged, idempotent deployment step. (safe-retraction/install! conn) @(d/transact conn (safe-retraction/retract-entity-tx-data [:eacl/id "acme"]))
The target can be a numeric entity ID or a valid lookup ref. Multiple and repeated invocations compose in one transaction:
@(d/transact conn [[:eacl.fn/retractEntity 1] [:eacl.fn/retractEntity 2] [:eacl.fn/retractEntity 1]])
A numeric entity ID can repair peer-side ghosts after an earlier native retraction. A lookup ref that no longer resolves cannot reveal the former entity ID, so it cannot perform that repair.
Do not add relationships involving a target in the same application
transaction that safely retracts it. Prefer delete-object! for very
high-degree targets so cleanup can be batched.
Use the backend's safe-retraction/support-descriptor before choosing a
Datahike or DataScript deployment mode. Installation, direct-mode examples,
restore behavior, integrity reports, and repair tools are documented in the
adapter guides:
Schema Syntax
EACL parses a documented subset of the SpiceDB schema DSL. Use
eacl/write-schema! to define your schema.
EACL's parser requires each relation or permission declaration to end at a
newline; put the next declaration and the definition's closing brace on a later
line. Empty definitions may still use the compact definition user {} form.
(eacl/write-schema! acl "definition user {} definition account { relation owner: user permission admin = owner } definition server { relation account: account permission admin = account->admin }")
Example Schema
Here's a complete example of defining a schema with eacl/write-schema!:
(eacl/write-schema! acl "definition user {} definition platform { relation super_admin: user } definition account { relation platform: platform relation owner: user permission admin = owner + platform->super_admin } definition server { relation account: account relation shared_admin: user permission reboot = account->admin + shared_admin }")
This schema defines:
platformresources can havesuper_adminusersaccountresources can have aplatformandowner, withadminpermission granted to owners and platform super_adminsserverresources belong to anaccountand can haveshared_adminusers, withrebootpermission granted to account admins and shared_admins
Now you can transact relationships. The usual way is eacl/create-relationships! against existing entities (see Quickstart). To create entities and relationships in the same transaction, use eacl.datomic.impl/tx-relationship with {:allow-tempids? true} — tempid pass-through is opt-in because a typo'd ID would otherwise silently create a ghost entity:
(require '[eacl.datomic.impl :as impl]) (let [db (d/db conn)] @(d/transact conn (concat [{:db/id "user1-tempid" :eacl/id "user1"} {:db/id "account1-tempid" :eacl/id "account1"}] (impl/tx-relationship db (impl/Relationship (spice-object :user "user1-tempid") :owner (spice-object :account "account1-tempid")) {:allow-tempids? true}))))
Limitations, Deficiencies & Gotchas:
- Exact snapshots require backend history:
at-exact-snapshotand continued cursors require the backend to reconstruct the selected database value. Ordinary Datomic history and history-enabled Datahike do not age-expire. History-disabled Datahike can lose a conditionally retained commit and then returns snapshot-unavailable rather than silently using a newer value. - History destruction is a lifecycle boundary: Datomic excision and Datahike purge/cutoff, branch force, reset, restore, or equivalent destructive replacement require quiescing affected traffic, completing the operation, rotating the shared source lifecycle and affected clients/caches, and then resuming with deliberate token/cursor key-version policy.
- No negation operator: EACL only supports Union (
+) permission operators, not-negation, e.g.permission admin = owner + shared_adminis valid,- but
permission admin = owner - banned_memberis not.
- Arrow syntax is limited to one level of nesting, e.g.
permission arrow = relation->via-permissionis supported,- but
permission arrow = relation->subrelation->permissionis not. The target permission may itself contain an arrow, so longer graph traversals can be modelled through named permissions.
- SpiceDB
subject#relationsubject sets are not supported. Model group membership with explicit group Relationships and arrow permissions when that expresses the required semantics. - Expansion is structural, not a membership proof: permission trees preserve
relation, permission, union, and arrow boundaries. Use
can?for an authorization decision. - Cache coherence requires EACL authorization writers: Bypassing EACL for schema, relationship, permissioned identity, or deletion mutations can leave cached answers stale. Stop affected traffic, repair the data, and expire every affected client before resuming.
- Deleting entities: Native entity retraction does not remove the
relationship stored at the other endpoint. Delete relationships first with
delete-object!, or use the optional safe-retraction function — see Deleting a permissioned entity. - Recursive permissions have safety limits: use
:count-limitto bound counts, and raise recursive traversal limits only after load testing. If a cached continuation is unavailable, EACL may replay earlier traversal work to continue a cursor. - Return order: EACL makes no global, lexical, or cross-backend ordering promise. For a fixed query and authenticated cursor walk, permission lookups use the sealed plan's stable first-discovery order and relationship reads use backend tuple-index order. This stability is sufficient for a cursor walk with no movement or duplicates; sort by a domain key after reading if presentation order matters.
Differences from SpiceDB
EACL follows SpiceDB's schema vocabulary and shared authorization semantics, but it is not a byte-for-byte or operational clone:
- Result order is backend-defined. Compare lookup and relationship results as sets unless your application explicitly sorts them; never compare EACL and SpiceDB page membership or cursor bytes.
- EACL cursors bind the selected native revision and its dependency/order proof. A cursor walk stays on that exact snapshot. If the backend cannot reconstruct it, EACL fails closed. A relevant write does not silently change page membership midway through a cursor walk.
- Omitted consistency means
:minimize-latency. EACL selects the current immutable database value visible to the local backend connection. SpiceDB may use an optimized cached revision, so freshness can differ. Use each backend's own causal token withat-least-as-freshorat-exact-snapshotwhen the distinction matters; tokens and cursors are backend-local. - EACL provides
count-resources,count-subjects, a controllable EACL result cache, anddelete-object!, which removes both stored Relationship halves. Datomic commits high-degree deletion in batches of 1,000; Datahike and DataScript use one atomic transaction. These do not have direct SpiceDB API equivalents. - EACL currently supports a smaller schema subset: unions and its documented arrow forms, but not caveats, wildcard subjects, expiration, intersections, exclusions, or subject relations.
- EACL evaluates relationship cycles as a fixed point and has no separate
dispatch-depth limit for checks, lookups, and counts. These operations remain
subject to configured traversal work limits. SpiceDB uses a configurable
dispatch-depth limit, which defaults to 50 and can return a maximum-depth
error for deep or cyclic data, so the two systems can differ on those graphs.
Only
expand-permission-treerefuses cycles (:eacl.permission-tree/cycle-detected) and depth beyond:permission-tree-limits(:max-depth 50by default). - Object identifiers are arbitrary non-empty strings and schema names follow the parser's grammar rather than SpiceDB's exact identifier and name grammars. A schema or dataset that must also load into SpiceDB should follow SpiceDB's stricter identifier and schema-name rules rather than relying on EACL's broader parser.
- A relation name is accepted only in the
:permissionslot ofexpand-permission-tree;can?,check-permission, the lookups and the counts require a permission (SpiceDB accepts either). - A relationship filter containing
:subject/idmust also contain:subject/type. This fails closed instead of interpreting one external ID across every subject definition.
Funding
Some of this open-source work was generously funded by my former employer, CloudAfrica.
Licence
- EACL is licensed under the Eclipse Public License v2.0.