Datahike is highly configurable to support different deployment models and use cases. Configuration is set at database creation and cannot be changed afterward (though data can be migrated to a new configuration).
Datahike uses the environ library for configuration, supporting three methods:
This allows flexible deployment: hardcode configs in development, use environment variables in containers, or Java properties in production JVMs.
The minimal configuration map includes:
{:store {:backend :memory ;keyword - storage backend
:id #uuid "550e8400-e29b-41d4-a716-446655440020"} ;UUID - database identifier
:name nil ;string - optional database name (auto-generated if nil)
:schema-flexibility :write ;keyword - :read or :write
:keep-history? true ;boolean - enable time-travel queries
:attribute-refs? false ;boolean - use entity IDs for attributes (Datomic-compatible)
:index :datahike.index/persistent-set ;keyword - index implementation
:store-cache-size 1000 ;number - store cache entries
:search-cache-size 10000} ;number - search cache entries
Quick start with defaults (in-memory database):
(require '[datahike.api :as d])
(d/create-database) ;; Creates memory DB with sensible defaults
Datahike supports multiple storage backends via konserve. The choice of backend determines durability, scalability, and deployment model.
Built-in backends:
:memory - In-memory (ephemeral):file - File-based persistent storageExternal backend libraries:
For detailed backend selection guidance, see Storage Backends Documentation.
When using environment variables or Java system properties, name them like:
| properties | envvar |
|---|---|
| datahike.store.backend | DATAHIKE_STORE_BACKEND |
| datahike.store.username | DATAHIKE_STORE_USERNAME |
| datahike.schema.flexibility | DATAHIKE_SCHEMA_FLEXIBILITY |
| datahike.keep.history | DATAHIKE_KEEP_HISTORY |
| datahike.attribute.refs | DATAHIKE_ATTRIBUTE_REFS |
| datahike.name | DATAHIKE_NAME |
etc.
Note: Do not use : in keyword strings for environment variables—it will be added automatically.
Ephemeral storage for testing and development:
{:store {:backend :memory
:id #uuid "550e8400-e29b-41d4-a716-446655440021"}}
Environment variables:
DATAHIKE_STORE_BACKEND=memory
DATAHIKE_STORE_CONFIG='{:id #uuid "550e8400-e29b-41d4-a716-446655440021"}'
Persistent local file storage:
{:store {:backend :file
:path "/var/db/datahike"}}
Environment variables:
DATAHIKE_STORE_BACKEND=file
DATAHIKE_STORE_CONFIG='{:path "/var/db/datahike"}'
High-performance local storage via datahike-lmdb:
{:store {:backend :lmdb
:path "/var/db/datahike-lmdb"}}
PostgreSQL or other JDBC databases via datahike-jdbc:
{:store {:backend :jdbc
:dbtype "postgresql"
:host "db.example.com"
:port 5432
:dbname "datahike"
:user "datahike"
:password "secret"}}
AWS S3 storage via konserve-s3:
{:store {:backend :s3
:bucket "my-datahike-bucket"
:region "us-east-1"}}
Memory hierarchy (e.g., Memory → IndexedDB for browsers):
{:store {:backend :tiered
:id #uuid "550e8400-e29b-41d4-a716-446655440022"
:frontend-config {:backend :memory
:id #uuid "550e8400-e29b-41d4-a716-446655440022"}
:backend-config {:backend :indexeddb
:name "persistent-db"
:id #uuid "550e8400-e29b-41d4-a716-446655440022"}}}
;; All :id values must match for konserve validation
For complete backend options and selection guidance, see Storage Backends.
Optional identifier for the database. Auto-generated if not specified. Useful when running multiple databases:
{:name "production-db"
:store {:backend :file :path "/var/db/prod"}}
Controls when schema validation occurs:
:write (default): Strict schema—attributes must be defined before use. Catches errors early.:read: Schema-less—accept any data, validate on read. Flexible for evolving data models.{:schema-flexibility :read} ;; Allow any data structure
With :read flexibility, you can still define critical schema like :db/unique, :db/cardinality, or :db.type/ref where needed.
See Schema Documentation for details.
Enable historical query capabilities:
{:keep-history? true} ;; Default: true
When enabled, use history, as-of, and since to query past states:
(d/q '[:find ?e :where [?e :name "Alice"]] (d/as-of db #inst "2024-01-01"))
Disable if: You never need historical queries and want to save storage space.
See Time Variance Documentation for time-travel query examples.
Store attributes as entity IDs (integers) instead of keywords in datoms for performance and Datomic compatibility:
{:attribute-refs? true} ;; Default: false
How it works:
Without attribute references (default):
;; Datoms store attribute keywords directly
#datahike/Datom [1 :name "Alice" 536870913 true]
With attribute references enabled:
;; Datoms store attribute entity IDs (integers)
#datahike/Datom [1 73 "Alice" 536870913 true] ;; where 73 is the entity ID for :name
Benefits:
Considerations:
:schema-flexibility :write (cannot use with :read)Example:
;; Create database with attribute references
(def cfg {:store {:backend :memory
:id #uuid "550e8400-e29b-41d4-a716-446655440000"}
:attribute-refs? true
:schema-flexibility :write})
(d/create-database cfg)
(def conn (d/connect cfg))
;; Use normal keyword syntax in transactions and queries
(d/transact conn [{:db/ident :name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}])
(d/transact conn [{:name "Alice"}])
;; Queries use keywords as usual - translation happens automatically
(d/q '[:find ?n :where [?e :name ?n]] @conn)
;; => #{["Alice"]}
;; But internally, datoms store integer attribute IDs for performance
When to use:
:attribute-refs? true for production databases (recommended for performance):attribute-refs? false only if you need :schema-flexibility :read or have specific compatibility requirementsChoose the underlying index implementation:
{:index :datahike.index/persistent-set} ;; Default (recommended)
Available indexes:
:datahike.index/persistent-set - Default, actively maintained, supports all features:datahike.index/hitchhiker-tree - Legacy, requires explicit library and namespace loadingMost users should use the default. Hitchhiker-tree is maintained for backward compatibility with existing databases.
On request-priced object stores (S3, R2, Tigris, GCS) the number of objects
written per commit dominates cost and latency. Three opt-in, per-store options —
diff buffering (:index-config {:diff-buf-size N}), root fusion
(:fuse-index-roots? true), and commit-graph opt-out (:commit-graph? false) —
cut that object count, approaching a single write per small commit. Each is
create-time-fixed and adopted from the store on reconnect. See Reducing write
amplification for how they work and when to enable
them.
For distributed deployments, configure a writer to handle all transactions while readers access storage directly via Distributed Index Space.
:writer-ownership :shared)Datahike's local writer ({:backend :self}) defaults to shared ownership: it
re-reads the branch head before each batch rather than assuming this JVM owns
the branch exclusively. Serverless runtimes make that safety important. Each AWS Lambda execution
environment is a separate JVM that believes it is the only writer, and Lambda
keeps several warm and routes to them alternately. With exclusive ownership,
each one would commit on top of its own stale head and silently overwrite the
other's transactions — no error, lost data.
{:store {:backend :s3 :bucket "my-bucket"}
:writer {:backend :self}} ; :writer-ownership :shared is the default
With shared ownership the writer re-reads the branch head from storage before
each batch of transactions, so they are applied to whatever is actually
stored, and @conn reads through to storage as well.
:db-after, so commit batching survives: a burst of 500 concurrent
transactions costs ~9 head reads and ~20 commits, not 500 of each. The chain
is bounded and never waits for more work to arrive, so it costs no latency
— a caller that awaits each transaction before issuing the next has nothing
to batch and does pay one read per commit.:exclusive: do this only when one process exclusively owns the
writer and avoiding the branch-head GET is worth the weaker safety.Shared ownership re-reads the branch head, and datahike also writes it back conditionally: the commit lands only if the head is still the one that was read. If another process moved it in between, this commit is rejected rather than overwriting theirs, and the transaction is re-applied against the new head. Nothing is lost and nothing is partially applied — the values a commit writes before the head flip are immutable and content-addressed, so a rejected commit leaves collectable orphans, never a dangling pointer.
This needs a store that can compare-and-set. Konserve reports how far its guarantee reaches as a domain:
| Domain | Meaning | Stores |
|---|---|---|
:process | threads in one JVM | memory |
:machine | processes on one host | filestore (OS advisory file lock) |
:global | processes on any host | S3 (If-Match on the object) |
Fencing is used automatically when the store offers it and skipped when it does not, which keeps single-writer setups working unchanged on every backend. If your deployment depends on it, say so — otherwise a store that cannot fence degrades quietly to the unconditional write, which is exactly the failure the mechanism exists to remove:
{:store {:backend :s3 :bucket "my-bucket"}
:writer {:backend :self
:writer-ownership :shared
:require-fencing :global}} ; refuse to connect without it
:require-fencing names the domain the deployment needs — :machine for
several processes on one host (dthk from two shells), :global for several
hosts (Lambda on S3). A store offering more than asked passes. It requires
:writer-ownership :shared: an exclusive writer never re-reads the head, so it has no
revision to fence against and the option would be inert.
The experimental self-writer :streaming? option from #959 remains a deprecated
compatibility alias (false means :shared, true means :exclusive).
Streaming itself remains a writer capability: both self writers stream their own
completed writes into the connection, Kabel streams synchronized remote writes,
and HTTP does not stream updates.
Three further knobs, all optional:
| Key | Default | Meaning |
|---|---|---|
:head-conflict-retries | 3 | How many times a rejected transaction is re-applied against the re-read head before the caller is told. 0 reports :datahike/head-conflict immediately, which is what you want if the caller must see every conflict. |
:head-conflict-backoff-ms | 25 | Base for the jittered exponential backoff between retries. |
:max-batch | 64 | Upper bound on transactions chained into one commit. |
Only transact! and load-entities are retried. Anything that merges branches
carries a conflict that belongs to the caller, and re-running it against a head
that moved would silently change what the merge means.
Branch lifecycle operations use the same store capability directly. Creating a
branch conditionally publishes its head; creating, deleting or forcing a branch
updates the shared :branches GC whitelist with a CAS loop; and database
creation conditionally claims the initial :db head. Stores without revisions
retain the historical best-effort behavior. force-branch! remains a deliberate
reset operation: it retries until its overwrite can be linearized, so use it as
exclusive administration rather than ordinary application traffic.
{:store {:backend :file :path "/shared/db"}
:writer {:backend :datahike-server
:url "http://writer.example.com:4444"
:token "secure-token"}}
Clients connect and transact through the HTTP server. Reads happen locally from shared storage.
Real-time reactive updates via WebSocket:
{:store {:backend :indexeddb :name "app-db" :id store-id}
:writer {:backend :kabel
:peer-id server-peer-id
:local-peer @client-peer}} ;; Set up via kabel/distributed-scope
Enables browser clients with live synchronization. See Distributed Architecture for setup details.
Access specific database branches (git-like versioning):
{:store {:backend :file :path "/var/db"}
:branch :staging} ;; Default branch is :db
Create and merge branches for testing, staging, or experiments. See Versioning for the branching API.
Send all operations (reads and writes) to a remote server:
{:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440023"}
:remote-peer {:backend :datahike-server
:url "http://server.example.com:4444"
:token "secure-token"}}
Useful for thin clients or when you want centralized query execution. See Distributed Architecture for RPC vs. DIS trade-offs.
Seed the database with schema or data on creation:
{:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440024"}
:initial-tx [{:db/ident :name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :email
:db/valueType :db.type/string
:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one}]}
Convenient for testing or deploying databases with predefined schema.
{:store {:backend :file
:path "/var/datahike/production"
:id #uuid "550e8400-e29b-41d4-a716-446655440000"}
:name "production-db"
:schema-flexibility :write
:keep-history? true
:attribute-refs? false
:index :datahike.index/persistent-set
:store-cache-size 10000
:search-cache-size 100000
:initial-tx [{:db/ident :user/email
:db/valueType :db.type/string
:db/unique :db.unique/identity
:db/cardinality :db.cardinality/one}]
:writer {:backend :datahike-server
:url "http://writer.example.com:4444"
:token "secure-token"}
:branch :db}
Prior to version 0.3.0, Datahike used URI-style configuration. This is still supported but deprecated in favor of the more flexible hashmap format.
Old URI format:
"datahike:memory://my-db?temporal-index=true&schema-on-read=true"
New hashmap format (equivalent):
{:store {:backend :memory :id #uuid "550e8400-e29b-41d4-a716-446655440025"}
:keep-history? true
:schema-flexibility :read}
Key changes:
:temporal-index → :keep-history?:schema-on-read → :schema-flexibility (:read or :write):store map:host/:path → :idExisting URI configurations continue to work—no migration required unless you need new features.
Can you improve this documentation? These fine people already did:
Konrad Kühne, Timo Kramer, Christian Weilbach, Judith Massa, Judith & JCEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |