🦅 EACL: Enterprise Access ControL
EACL is a situated ReBAC authorization library inspired by SpiceDB, built in Clojure and backed by Datomic Pro, Datahike, DataScript, or a qualified embedded Datalevin deployment.
Is it any good?
Yes.
Demos
Try the EACL Demo at demo.eacl.dev with options for:
- Backend: Datomic Pro, Datahike, Datalevin or DataScript (in-browser)
- Storage: S3 or DynamoDB
- Execution: AWS Lambda or EC2 (t3.small instance)
EACL Drive is a toy clone of Google Drive that shows how easy it is to add fine-grained permissions to your app.
Overview
| Authentication (AuthN) | Authorization (AuthZ) |
|---|---|
Who are you?, i.e. who is the <subject>? |
What can <subject> do? |
EACL is concerned with fast & correct authorization, i.e. permissions.
EACL permissions are just data that co-exist with your application data – hence, situated, which offers several benefits, notably:
- Reduced network latency,
- Strong local consistency,
- Horizontal scaling because queries run in-process on the Peer, and
- Real-time UI view maintenance with near-optimal deltas.
🦅 EACL is pronounced "EE-kəl", like "eagle" with a k because as a situated AuthZ system, EACL monitors the situation 🥁.
Next Steps
- To get started with EACL, see Quickstart →
- To see the supported backends, see Supported Backends →
- To learn how to use EACL, see EACL API →
- To understand the data structures, see Data Structures →
- To understand the consistency modes, see Consistency Semantics →
- To understand how the EACL cache works, see Caching →
Quickstart
Datomic Pro
You need the Clojure CLI and Java 25 or newer for this published build. The Datomic Peer dependency is included; this memory example needs no separate Datomic server or account.
Create a directory with this deps.edn:
{:deps {dev.eacl/eacl-datomic {:mvn/version "8.0.0-RC-2026-10-02"}}}
This is a release candidate (a preview release). See
Clojars for published versions.
Start a REPL in that directory with clojure -M, then evaluate:
(require '[datomic.api :as d]
'[eacl.core :as eacl]
'[eacl.datomic.core :as eacl.datomic]
'[eacl.datomic.schema])
(def uri (str "datomic:mem://eacl-" (random-uuid)))
(d/create-database uri)
(def conn (d/connect uri))
;; Install EACL's schema, then your application's schema.
(eacl.datomic.schema/install! conn)
@(d/transact conn
[{:db/ident :app/id
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :document/title
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}])
(def acl (eacl.datomic/make-client conn
{:object-id->lookup-ref (fn [id] [:app/id id])
:entid->object-id (fn [db eid] (:app/id (d/entity db eid)))}))
(eacl/write-schema! acl
"definition user {}
definition document {
relation viewer: user
relation owner: user
permission view = viewer + owner
permission share = owner
}")
@(d/transact conn
[{:app/id "alice"}
{:app/id "bob"}
{:app/id "report" :document/title "Report"}])
(def alice (eacl/spice-object :user "alice"))
(def bob (eacl/spice-object :user "bob"))
(def report (eacl/spice-object :document "report"))
(eacl/create-relationship! acl alice :viewer report)
(eacl/can? acl alice :view report) ; true
(eacl/can? acl bob :view report) ; false
(mapv :id (:data (eacl/lookup-resources acl
{:subject alice :permission :view :resource/type :document :first 10})))
;; => ["report"]
Give each user and document a unique, stable ID owned by your application.
This example uses :app/id; you do not need to put application IDs in EACL's
internal schema.
The default token keys are process-local. For keys that survive restarts or work across load-balanced Peers, see security keys. Leave cache options out to use the defaults.
To delete a secured entity in Datomic, use :eacl.fn/retractEntity. Install
the function during database setup; it removes the entity and its relationships
in one transaction:
(require '[eacl.datomic.safe-retraction])
(eacl.datomic.safe-retraction/install! conn)
@(d/transact conn [[:eacl.fn/retractEntity [:app/id "report"]]])
(eacl/can? acl alice :view report) ; false
Do not use ordinary entity retraction while EACL relationships still refer to the entity. See safe deletion.
- Run the complete consumer checks.
- Combine application data and relationships in one transaction.
- Set an expiration date on a share.
- Upgrade an existing application.
- Datahike quickstart or DataScript quickstart.
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. |
| Datalevin | eacl-datalevin — implemented, publication pending |
Embedded LMDB; storage-enforced write policy and ordered generations. |
S3-backed Datahike is attractive for infrequently-accessed apps, because you can trade latency for reduced storage cost, and it supports serverless to reduce running Peer / Transactor costs.
Note: DataScript and Datalevin have no at-exact-snapshot semantics. Datahike requires a retained commit graph or temporal history to support exact snapshots. Datalevin uses scalar ordered-generation proofs from the maintained fork, so completed answers may reuse across unrelated forward revisions without historical selection.
Overview of EACL
- EACL is inspired by SpiceDB:
- SpiceDB is the most faithful open-source implementation of Google Zanzibar.
- Zanzibar powers Google Drive, YouTube, Gmail and Google Calendar, serving billions of authorization requests per day over billions of Relationships.
- SpiceDB is used at OpenAI, Reddit and Netflix.
- EACL operates at a different scale from SpiceDB:
- Spice is benchmarked against 100B Relationships
- EACL aims for ~10M Relationships or less in a situated environment – potentially 100M
- By adopting a ReBAC data model, you can avoid a rewrite later and easily migrate to SpiceDB when you achieve hyperscale.
- EACL uses formal verification techniques to enforce correctness.
- EACL is fast with a cache that benefits from a strong concept of time (snapshot basis) in immutable, single-writer ACID databases like Datomic or Datahike, i.e. we leverage monotonic basis derived from
tin[e a v t]. - EACL supports SpiceDB's consistency semantics, with some backend-specific limitations.
- EACL is black-box tested against SpiceDB, but with a different lookup return order (by design) because EACL is a different implementation.
- The cost of building EACL over 18+ months by an experienced engineer is estimated at ~$80k–$120k.
- The first versions of EACL were tradcoded (pre-AI).
- Using 2026+ frontier models, you may be able to rebuild EACL for <$50k, but good luck finding a cleanroom model, because every model since has been trained on EACL's & SpiceDB's source code.
- If you decide to roll your own AuthZ, you will have to go through the same optimizations that EACL already has, but do not attempt to use Datalog child rules, because they do not scale (that was EACL v1).
This README is too long & too technical, so I am working to simplify it and break it out into area-specific documents. Despite my best attempts, EACL has become a large project.
Table of Contents
- 🦅 EACL: Enterprise Access ControL
- Next Steps
- Is it any good?
- Supported Backends
- Overview of EACL
- Table of Contents
- Project Status
- Real-Time UI Maintenance
- EACL API
- Snapshots
- Rationale
- The Benefits of Situated Authorization
- ReBAC: Relationship-based Access Control
- Consistency Semantics
- Data Structures
- Performance
- Formal Verification
- Example Schema
- Modules
- Quickstart
- EACL Schema
- EACL ID Configuration
- Caching
- Schema Syntax
- Example Schema
- Limitations, Deficiencies & Gotchas:
- Differences from SpiceDB
- Funding
- Licence
Project Status
Warning
EACL is used in production, but under active development. The examples use
8.0.0-RC-2026-10-02, a release candidate. See Clojars for published versions.
Real-Time UI Maintenance
Consider 10,000 online users: how often should clients re-query to keep their UIs up-to-date?
- If they poll on a schedule, clients are always out-of-date (eventually consistent).
- Refreshing each user's view on every DB write does not scale. In a single-writer system, more users means more frequent writes, i.e.
tx_rate(num_users, tx_rate_per_user). - More frequent writes means more queries, so
num_users * tx_rate(num_users, tx_rate_per_user) * queries_per_viewquickly becomes a Read Amplification problem that can dramatically lower Peer performance, or require horizontal scaling.
What if you could compute exactly which users are affected by every DB write and notify only those clients, in real-time?
- Well, EACL already knows which resources each subject (or principal) can access based on your permission schema.
- The EACL backends support transaction listeners via
d/listen, so you can inspecttx-datafor every transaction, and call EACL's efficienteacl/lookup-subjectsto retrieve a list of users who can see the affected resource, filter it down to online users and notify them. Alternatively, calleacl/can?for each online user in parallel. - To compute perfect viewership for complex nested queries would require Different Dataflow, but you can get 80-90% of the way there by leveraging the permission graph – as long as mutations touch a resource that the principal can see.
- EACL cursors do not expire, because we can leverage time in our supported backends and ensure cache coherence.
EACL API
EACL implements an idiomatic IAuthorization protocol for each supported backend, which extends the SpiceDB gRPC API.
Checking Permissions
EACL can efficiently answer questions like, "Can <subject> do <permission> on <resource>?"
(eacl/can? acl subject permission resource ?consistency)
=> true | false
; e.g.
(eacl/can? acl (->user "alice") :view (->server "server1") eacl.spicedb.consistency/fully-consistent)
=> true | false
If you need cache provenance, use check-permission instead of can?, otherwise they are equivalent:
(eacl/check-permission acl
{:subject subject
:permission permission
:resource resource
:consistency eacl.spicedb.consistency/fully-consistent})
=> {:allowed? true, :cached? boolean, :cache-basis ...}
Lookups
"Which <resources> does <subject> have <permission> on, as-of <10 seconds ago, or newer>?"
(eacl/lookup-resources acl
{:subject subject
:permission permission
:resource/type resource-type
:first page-size ; or :last page-size
:consistency (eacl.spicedb.consistency/at-least-as-fresh token-10s-ago)})
=> {:data [{:type :product :id "product-1"}
{:type :product :id "product-7"}
...
{:type :product :id "product-63"}]
:page-info ...
:cached? true|false>
...}
The :consistency argument is optional. The default is minimize-latency,
which means locally-consistent to the Peer. at-least-as-fresh selects an
appropriate backend basis directly and requires no cache-checkpoint option.
(def token-10s-ago (eacl.datomic/zed-token-at-least-seconds-ago acl 10))
(eacl/lookup-resources acl
{:subject (->user "alice")
:permission :view
:resource/type :product
:first 50
:consistency (eacl.spicedb.consistency/at-least-as-fresh token-10s-ago)})
=> {:data [{:type :product :id "product-1"}
{:type :product :id "product-7"}
...
{:type :product :id "product-63"}]
:page-info ...
:cached? true|false>
...}
(eacl/lookup-resources acl query)
=> {:data [resources...] :page-info {...} :cached? boolean :cache-basis ...}
(eacl/lookup-subjects acl query)
=> {:data [subjects...] :page-info {...} :cached? boolean :cache-basis ...}
Counting
SpiceDB does not support counting (you must traverse in pages), but EACL does.
(eacl/count-resources acl query)
=> {:count 42, :limit -1, :cached? boolean, :cache-basis ...}
(eacl/count-subjects acl query)
=> {: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.
Note: the default :limit will soon change to 50k instead of -1 (infinite), because high count-limits can exhaust Peers and trigger costly I/O from storage, esp. in recursive schemas.
Snapshots
An EACL client reads the current database. A snapshot holds one database version and one evaluation time, so several reads see the same state.
(eacl/with-snapshot [s (eacl/snapshot acl)]
[(eacl/can? s alice :view report)
(eacl/lookup-resources s
{:subject alice :permission :view :resource/type :document})])
with-snapshot releases the snapshot when the body finishes, including when
it throws. If you retain one manually, call eacl/release! when finished.
A read after release raises :eacl/snapshot-released. Datalevin snapshots
must be used and released on the platform thread that acquired them.
A retained snapshot keeps its captured time. An expiring share can therefore still grant access in an old snapshot after its deadline. Use the client, or acquire a new snapshot, when checking current access.
To select a historical version, pass a consistency descriptor to
eacl/snapshot. The backend must support that selection. A consistency
option on an existing snapshot only checks that it meets the requested
condition; it cannot move the snapshot to another version.
You can also preview changes without committing them:
(eacl/with-snapshot [base (eacl/snapshot acl)]
(let [tx (eacl/tx-relationship base :delete alice :viewer report)]
(eacl/with-snapshot [preview (eacl/with base tx)]
(eacl/can? preview alice :view report))))
Use eacl/with-schema to preview a permission schema. Preview results do not
become shared cached answers. Use these public helpers instead of wrapping a
raw Datomic d/with or d/filter value in an implementation-level client.
See atomic writes and the backend guide for supported snapshot operations and backend limits.
Rationale
I spent the better half of 2024 integrating SpiceDB at [