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
- Request and success response bodies are MessagePack (
Content-Type: application/msgpack). - Error responses are JSON (
application/json):{"code": "...", "message": "..."}. - Database ids and replica (site) ids are 16 bytes;
{id}in paths is the hex database id. Database ids are random, except the vault catalog’s, which is derived from the vault secret. - Request bodies are limited to 16 MiB.
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:
- a read key, derived from the database key, which every member has;
- a write key, derived from a separate write secret that read-only invites omit.
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
dataissignature || nonce || ciphertext: XChaCha20-Poly1305 over the padded batch, with the database id, replica id, sequence number and epoch bound as associated data, signed with the database write key (Ed25519). A relay can’t alter, re-label or move a batch without detection, and only members with write access can produce one.- The relay assigns each stored batch a cursor, increasing per database.
headis the highest cursor. - Pushed batches must use the database’s current epoch (
409otherwise). A batch whose(site, seq)is already stored is skipped;acceptedcounts the new ones. - Clients pull with
after=<cursor>(default 0) andlimit(default 500, at most 5000). Responses are capped at about 8 MiB;moremeans another page is available. wait(seconds, at most 30) long-polls until a batch arrives pastafteror the wait expires.botdb sync --watchwaits up to 25 seconds per request.- If the relay returns
404for a database the client had already synced, or itsheadis behind the client’s cursor, the client assumes the relay lost its data and pushes everything again.
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.