botdb

docs / reference

Relay protocol

The HTTP API between botdb clients and relays, request signing, and how batches, cursors and snapshots work.

The relay is deliberately simple. It stores opaque encrypted batches per database in arrival order, serves them by cursor, keeps the latest encrypted snapshot and daily encrypted backups, and checks that each request is signed by a key the database registered. It never decrypts anything.

This page summarizes protocol version 1.

Encoding

Endpoints

Method Path Role Body / response
GET /health none ok
PUT /v1/dbs/{id} write Register: RegisterRequest → {epoch, head}
POST /v1/dbs/{id}/batches write Push: PushRequest → PushResponse
GET /v1/dbs/{id}/batches read/write Pull: ?after=&limit=&wait= → PullResponse
PUT /v1/dbs/{id}/snapshot write Upload snapshot: SnapshotBody → 204
GET /v1/dbs/{id}/snapshot read/write Download snapshot: SnapshotBody
POST /v1/dbs/{id}/rotate write New key epoch: RegisterRequest → 204
DELETE /v1/dbs/{id} write Delete the database and its data → 204
POST /v1/dbs/{id}/backups write Upload today’s backup: BackupBody → BackupInfo
GET /v1/dbs/{id}/backups read/write List backups: BackupList
GET /v1/dbs/{id}/backups/{day} read/write Download a backup: BackupBody

Authentication

Every database registers two Ed25519 public keys:

Each request carries three headers:

Header Value
x-botdb-role read or write
x-botdb-ts Client timestamp in Unix milliseconds
x-botdb-sig Base64 Ed25519 signature with the key for that role

The signature covers this message:

botdb-req/v1
{METHOD}
{path and query}
{timestamp}
{hex(sha256(body))}

The relay verifies the signature against the registered public key for the role and rejects timestamps more than 5 minutes from its own clock (401). Read endpoints accept either role; write endpoints answer 403 to a read-role request.

Registration is signed with the write key whose public half is in the request body. Registering again with the same keys and epoch is a no-op; different keys get 409. When the relay has a token (BOTDB_RELAY_TOKEN), registration also needs Authorization: Bearer <token> (401 token_required otherwise). Clients register lazily: a push or snapshot upload that gets 404 registers the database and retries.

Messages

RegisterRequest { read_pub: bytes(32), write_pub: bytes(32), epoch: u32 }

SealedBatch     { site: SiteId, seq: u64, epoch: u32, data: bytes }
PushRequest     { batches: [SealedBatch] }
PushResponse    { head: u64, accepted: u32 }

PulledBatch     { cursor: u64, batch: SealedBatch }
PullResponse    { batches: [PulledBatch], head: u64, floor: u64, epoch: u32,
                  snapshot_cursor: u64?, more: bool }

SnapshotBody    { epoch: u32, cursor: u64, data: bytes }

BackupBody      { data: bytes }
BackupInfo      { day: u64, created_at: u64, size: u64 }   // day: days since 1970-01-01, UTC
BackupList      { backups: [BackupInfo] }                  // newest first

Batches and cursors

Snapshots and compaction

A client uploads an encrypted snapshot of the merged state once the relay holds 1000 or more batches past its latest snapshot (head - snapshot_cursor). The snapshot is tagged with the cursor it covers, must use the current epoch, and its associated data binds it to the database id and epoch. The relay keeps one snapshot per database.

On upload, the relay deletes batches covered by the snapshot that are older than its retention period (BOTDB_RELAY_RETENTION_DAYS, default 7) and raises floor, the cursor at or below which batches may be gone. A client whose cursor is below floor, such as a new device, downloads the snapshot first and then pulls the batches after it.

Backups

Backups are optional; a relay without them answers 404, and clients carry on. botdb cloud and botdb relay both keep them.

Once a day, after a successful sync, each device that can write uploads an encrypted snapshot file of the database (with history, the same format as local snapshots). The relay files it under the current UTC day. The first upload of a day is kept and later ones that day are ignored, so a backup never changes once it’s taken. The relay keeps the 7 most recent days that have a backup plus the newest backup of each of the 4 most recent weeks, and deletes the rest. Backups survive key rotation (they’re sealed with keys the database secret keeps) and are deleted with the database.

Quotas

With BOTDB_RELAY_MAX_DB_BYTES, a push that would exceed the limit gets 507. A snapshot upload may temporarily take a database to twice the limit, so compaction can still free space. Backups don’t count toward the limit, but one larger than twice the limit gets 507.

Key rotation

POST /v1/dbs/{id}/rotate, signed with the current write key, registers new read and write public keys for a higher epoch. The relay drops batches and snapshots from older epochs and moves floor to head, so holders of old keys can no longer read or write through it. The rotating client then re-uploads everything under the new key, followed by a fresh snapshot.

Status codes

Status Code Meaning
400 invalid Malformed id, body or keys.
401 unauthorized, token_required Bad signature, stale timestamp or missing token.
403 forbidden Write access required.
404 not_found, no_snapshot, no_backup Database not registered, no snapshot stored, or no backup for that day.
409 epoch, conflict Wrong key epoch, or already registered with other keys.
507 quota Storage limit reached.

Storage

The bundled relay stores everything, backups included, in a SQLite file, relay.db, in its data directory. botdb cloud keeps each database’s log in a Durable Object and its snapshot and backups in R2. Storage sits behind the RelayStorage trait, so other backends can be added without protocol changes.