CairnDB
A serverless database engine on blob storage — the bucket is the only server.
A cairn is a stack of stones raised one by one, by many independent hands, with no custodian — and it stands for centuries. CairnDB works the same way: all state lives in an object-storage bucket, every writer is just a library call, and the bucket itself arbitrates concurrency through conditional writes. No database server, no coordinator, no idle compute.
On that substrate CairnDB exposes the layers a database engine is made of:
| Layer | You get | Engine analogy |
|---|---|---|
| Objects | etag-guarded key-value documents (CAS, put-if-absent) | atomic page writes |
| Coordination | claim (unique constraint), lease (fenced ownership), doc (retrying read-modify-write) |
locks & constraints |
| Logs | named, append-only commit logs — dense, totally ordered, durable on ack | WAL |
| Transactions | optimistic multi-key atomicity, coordinated by a system log | transaction manager |
| Projections | deterministic replay into local read-only SQLite, snapshots, time travel | indexes & materialized views |
from cairndb import CairnDB
db = CairnDB.configure({"storage": {"type": "s3", "bucket": "myapp"}})
await db.objects.put("config/app.json", data) # conditional KV
result = await db.claim("dispatch/etl:2026-08-10", {"run": 1}) # exactly-one winner
lease = await db.lease("state/run-1", ttl=120) # fenced ownership
seq = await db.log("orders").append(event) # durable, ordered
async with db.transact() as tx: # multi-key atomicity
tx.put("accounts/alice", alice_bytes)
tx.put("accounts/bob", bob_bytes)
proj = db.projection("orders_view", log="orders") # SQL over the log
The documentation is published at https://quadratic-labs.github.io/cairndb/
(sources in docs/). Start with
the quickstart,
and see the concepts for the
semantics of each layer.
Use Cases
- Small to medium datasets (up to ~10 GB per projection) with complex read queries and low/medium write volume
- Strong auditability and determinism requirements: event sourcing, time-travel reads, rebuild-anywhere recovery
- Coordination state for serverless/scale-to-zero systems: workflow ownership, exactly-once dispatch, checkpoints
- "I want a database but refuse to run or rent a database server"
How It Works
writers ──PUT log/N+1 (if-absent)──▶ ┌────────────────────────┐
│ Blob storage bucket │
readers ◀──GET log/N+1 (poll)──────── │ log/000000000042.msgpack
│ logs/{name}/... │
│ snapshots/v1/...sqlite │
snapshot job (cron) ◀──replay──────▶ └────────────────────────┘
- Every write batch becomes an immutable, numbered commit object. The
bucket accepts exactly one writer per number (
If-None-Match), so each log is dense, gap-free, and totally ordered — no clocks, no sequencer service. Coordination documents use the same conditional-write machinery with etags (compare-and-swap). - Clients replay commits through registered event handlers into a local SQLite file, swapped atomically so readers never see partial state.
- A scheduled job replays the log into snapshot files that bound client startup time; garbage collection prunes what snapshots cover. Nothing outside the bucket needs to survive.
Multiple concurrent writers are safe by construction: a writer that loses the race for commit N+1 fetches the winner's commits, optionally revalidates its events against them, and retries at N+2. A fenced lease holder's writes are rejected by the storage itself.
Installation
# Requires Python 3.14+
pip install cairndb # filesystem backend only
pip install cairndb[s3] # + Amazon S3 / S3-compatible
pip install cairndb[gcs] # + Google Cloud Storage
pip install cairndb[azure] # + Azure Blob Storage
pip install cairndb[cli] # + `cairndb` CLI (snapshot/gc/rebuild jobs)
Quick Start
import aiosqlite
from cairndb import CairnDB, Event, EventType, SchemaVersion, Timestamp
async def init_users(db_path: str) -> None:
async with aiosqlite.connect(db_path) as conn:
await conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
await conn.commit()
db = CairnDB.configure({"storage": {"type": "filesystem", "path": "./data"}})
# Write: durable and totally ordered as soon as append() returns
seq = await db.log("users").append(
Event(
event_type=EventType("user.created"),
timestamp=Timestamp.now(),
payload={"id": 1, "name": "Alice"},
schema_version=SchemaVersion("1"),
)
)
# Project: replay events into local SQLite, declaratively
proj = db.projection("users_view", log="users", init_schema=init_users)
@proj.on("user.created")
async def handle_user_created(conn, entry):
await conn.execute(
"INSERT INTO users (id, name) VALUES (?, ?)",
(entry.payload["id"], entry.payload["name"]),
)
await proj.wait_for(seq) # read-your-writes
with proj.connect() as conn: # read-only SQLite
rows = conn.execute("SELECT name FROM users").fetchall()
await db.close()
Coordination needs no handlers or projections — it is storage-level:
result = await db.claim(f"dispatch/{key}", {"run_id": run_id})
if result.won:
lease = await db.lease(f"state/{run_id}", ttl=120, holder=worker_id)
...work, calling await lease.renew() as a heartbeat...
await lease.release(state={"status": "done"}) # LeaseLost if we were fenced
Snapshots & retention (scheduled jobs)
export CAIRNDB_STORAGE_TYPE=s3 CAIRNDB_S3_BUCKET=my-bucket
cairndb snapshot --handlers myapp.projections:registry \
--init-schema myapp.projections:init_schema
cairndb snapshot --log orders --handlers myapp.projections:orders_registry
cairndb gc --keep-snapshots 3 # add --prune-log to drop covered history
See the quickstart for the full walkthrough,
including the lower-level Committer/HandlerRegistry API the facade is
built on.
Guarantees & Trade-offs
| Property | Guarantee |
|---|---|
| Durability | An acked write is in the bucket; there is no ack-before-durable window |
| Ordering | Total order per log, arbitrated by the bucket (no clocks) |
| Concurrency | Any number of writers; optimistic races with retry; epoch-fenced leases |
| Transactions | Optimistic multi-key atomicity; commit point is a log record; conflicting transactions abort |
| Read consistency | Eventual (poll interval); per-session read-your-writes via wait_for |
| Throughput ceiling | ~1 commit per storage round-trip per log (batching multiplies events/commit; shard across named logs) |
| Recovery | Everything outside the bucket is disposable and rebuilt by replay |
License
MIT © 2026 Thomas Zamojski
Release files for cairndb 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cairndb-0.4.1.tar.gz | 64.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cairndb-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 142.2 kB
Release files / cairndb-0.4.1.tar.gz
| Download URL | cairndb-0.4.1.tar.gz |
|---|---|
| Size | 64.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b688d55e0f439329be6f6c5e0be4ff094bbd18b7f90afb07e6ca2b074885c754
|
|
BLAKE2b-256 checksum How to use checksums |
586b7bc8bff9f53795549e1d90f69426122f76d7afd5ec19e398533398a3da31
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / cairndb-0.4.1-py3-none-any.whl
| Download URL | cairndb-0.4.1-py3-none-any.whl |
|---|---|
| Size | 78.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8f186ed4c95e3b62b855f2cebebbdc6f0382374d74c5a0c0889b5e07e0e3560c
|
|
BLAKE2b-256 checksum How to use checksums |
53666ec9fe8395699b78d75b2a57f00e8b54233b889092cef445427a6916dddf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log