New
Apps 90Makers 90This week 90
Promote your app−30%
0views in
the last 30 days

EACL

Situated ReBAC authorization library for Clojure, inspired by SpiceDB

Built with Claude Code

by theronic · from ZASouth Africa · added Oct 29, 2026
no ratings0 comments
Open app Code

🦅 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:

  1. Reduced network latency,
  2. Strong local consistency,
  3. Horizontal scaling because queries run in-process on the Peer, and
  4. 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

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.

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 t in [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.

Continue to Project Status →

Table of Contents

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_view quickly 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 inspect tx-data for every transaction, and call EACL's efficient eacl/lookup-subjects to retrieve a list of users who can see the affected resource, filter it down to online users and notify them. Alternatively, call eacl/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 [

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#authorization · #clojure · #rebac