Storage backends
A backend is one string. Change it and the same code writes to a local directory, a MinIO container, or a production bucket. There is no server component to run either way.
| Backend string | What it is | Python | JavaScript |
|---|---|---|---|
file:///abs/path | A directory on disk | Read + write | — |
memory:// | In-process, discarded on exit. For tests | Read + write | — |
http://host/bucket | Any S3-compatible endpoint | Read + write | Read, and write with a put backend |
https://host/bucket | The same, over TLS | Read + write | Read, and write with a put backend |
JavaScript always goes over HTTP. It has no filesystem path, so a dataset written to file:// has to be served before a browser or Node process can read it — and a plain static server can serve reads but not accept writes.
Local disk
The fastest way to work. Nothing to install, nothing to start:
To read that dataset from JavaScript, serve the directory:
--cors matters only for browsers; a Node script can read a server without it.
MinIO
An S3-compatible server in a container. Use it when you want to exercise the real HTTP path locally, or to share a dataset across machines on your network:
The backend string is then http://localhost:9000/demo. Making the bucket public keeps development simple; it also means anyone who can reach the port can read it.
S3, R2, B2, Wasabi
Any store that speaks S3-style GET and PUT works. Point at the bucket:
Writes are signed with SigV4, picked up from the standard environment variables:
No code changes are needed to move from MinIO to production. Set the variables and change the URL.
Backends in JavaScript
JavaScript takes a backend object rather than a URL string. Only get is required, and a get-only backend is a read-only backend — the write entry points check for put up front and refuse by name:
Passing null instead uses plain fetch against the URI's base, which is enough to read a public bucket:
range is half-open — [start, end), the shape the Rust core uses. An HTTP Range header is inclusive, so a fetch-based backend must send bytes=${start}-${end - 1}. Sending end fetches one byte too many.
getStream is optional. Existing backends remain readable through get; implementing a bounded stream lets DreamDB scan historical oversized inline Tracks without first aggregating the complete object in wasm memory. A structured get or getStream response should include totalLength when known so range verification can distinguish the returned slice from the complete object.
The bundled S3Backend is read-only. For server-side writes, a fetch wrapper of about twenty lines is enough — get with a Range header, head, and put translating ifMatch to If-Match and ifNoneMatchStar to If-None-Match: *, returning casFailed on a 412.
Writing from a browser
Credentials must not reach the page. PresignedBackend keeps them out: your server mints short-lived presigned PUT URLs, and performs the ref's compare-and-swap itself.
Anchors are nanoseconds, and in JavaScript you pass a bigint. A number is accepted only below Number.MAX_SAFE_INTEGER; above that the SDK throws instead of rounding.
Two rules for anything in front of a bucket
Range should be honoured. DreamDB reads exact vectors by byte offset, and asks for half-open [start, end) ranges that the HTTP layer sends as inclusive bytes=start-(end-1). A proxy or server that ignores Range and replies 200 with the whole object does not break correctness — the connectors detect it and slice locally — but every ranged read then transfers the whole object, which makes large datasets impractical. Such a backend also fails Range conformance and cannot claim the corresponding performance profile.
CORS must expose ETag. Refs advance by compare-and-swap, so the SDK needs to read the ref's current ETag. Without ExposeHeaders, the browser hides that header, the SDK falls back to create-only, and the second commit to any ref fails while the first succeeded:
Layout on disk
Whatever the backend, the object layout is the same, which is why a directory served over HTTP is indistinguishable from a bucket:
Nothing here is a database file. Every object is content-addressed and immutable except refs/<name>, which is a single pointer updated with compare-and-swap.