A database that is just a bucket

Most databases are a process you run. Datahike is a value you store — an immutable, persistent index that lives in a key-value store — and that changes what “deploying a database” has to mean. dthk is Datahike compiled to a native binary for Linux, macOS and Windows. Give it a directory, or an S3 bucket, and that is the whole installation.

unzip datahike-<version>-linux-amd64.zip && chmod +x dthk

cat > db.edn <<'EDN'
{:store {:backend :file :path "./mydb" :id #uuid "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}
 :keep-history? true
 :schema-flexibility :read}
EDN

./dthk create-database edn:db.edn
./dthk transact conn:db.edn '[{:name "Alice"} {:name "Bob"}]'
./dthk query '[:find ?n :where [?e :name ?n]]' db:db.edn

Every command is a fresh process. It opens the store, does its work, and exits. There is nothing to keep running between them.

What you get for that

The same database as the JVM library, because it is the JVM library, ahead-of-time compiled: Datalog queries, a full transaction history you can query with history:db.edn or asof:<tx>:db.edn, branches, and one file format shared with every other Datahike client. A Clojure service, a Python script through libdatahike, and a shell pipeline can all open the same directory.

Two writers, no server

The part that usually forces a server is writing. Two processes that both read the current state and both write a new one will lose one of the writes — unless something orders them. Datahike orders them in the store.

A commit is a new index value, written to storage, plus one pointer flip: the branch head. The head is written conditionally — it lands only if the head is still the one the writer read. If another process moved it in between, the commit is rejected and re-applied against the new head, up to three times with a jittered backoff. Nothing is lost and nothing is torn: the values a rejected commit wrote are content-addressed and immutable, so a losing writer leaves collectable garbage, never a dangling pointer.

What “conditionally” means depends on the store, and konserve — the storage layer — reports how far its guarantee reaches:

StoreMechanismReach
:filean OS file lock on a sidecar next to the headprocesses on one host
:s3If-Match on the object’s ETag, evaluated by S3processes on any host

So two shells on one machine are safe on the filesystem, and any number of Lambdas or containers are safe on a bucket. The default :writer-ownership :shared re-reads the head before every batch, which is what makes writers that alternate safe on any store; the conditional write is what makes writers that overlap safe on a store that can compare-and-set.

If your deployment depends on that, say so, and the CLI will refuse to connect to a store that cannot provide it:

What is this syntax?
{:store {:backend :s3, :bucket "my-bucket", :region "eu-central-1",
         :id #uuid "7a3e8400-e29b-41d4-a716-4466554407d1"},
 :writer {:backend :self, :writer-ownership :shared, :require-fencing :global}}
{:store  {:backend :s3 :bucket "my-bucket" :region "eu-central-1"
          :id #uuid "7a3e8400-e29b-41d4-a716-4466554407d1"}
 :writer {:backend :self
          :writer-ownership :shared
          :require-fencing :global}}   ; :machine for one host, :global for many

What it costs

Shared ownership costs one branch-head read per batch of transactions — roughly 10–40 ms and $0.0000004 on S3. Transactions already queued when one commits are chained onto it and share its read, so a burst of 500 concurrent transactions costs on the order of nine head reads and twenty commits, not five hundred of each. A caller that awaits every transaction before issuing the next has nothing to batch and pays one read per commit; that is the honest worst case, and it is the cost of a stat.

Storage is the only bill. A bucket with a few thousand small objects, or a directory, is the entire footprint of a database with its complete history.

Where the writer runs

None of this needs Datahike’s server. It needs a store that can compare-and-set, and every writer to open it with shared ownership. That is a different shape from the usual “one writer process, many readers” — it is no writer process, and any process may write, ordered by the store.

The moment you want more than that — a long-lived writer that batches on your behalf, a WebSocket that streams commits to browsers, a place to put authorization — that is what the Datahike server and the Kabel writer are for, and a :writer {:backend :datahike-server ...} entry in the same config file sends transactions there instead. The store, and the data in it, do not change.

Honest edges

The CLI documentation has the full command set, including forking and merging databases from the shell; the configuration reference has the writer and fencing options in detail.