Skip to content

Part 9 — blobs

Records are capped at 512 KB, and last-write-wins is a poor fit for a 20 MB payload. Large content goes in a blob, sealed the same way and addressed by a random ID that a record references.

const blobId = await engine.uploadBlob('g:acme', bytes, { policy: 'retain' });
await engine.put('g:acme', { kind: 'file', data: { blobId, name: 'plan.pdf' } });
const bytes = await engine.downloadBlob('g:acme', blobId);
await engine.ackBlob('g:acme', blobId); // "I have a durable copy"
policy the relay keeps the bytes until
retain somebody deletes them (default)
transport every member has acked a copy
ephemeral the TTL passes (ttlMs, default 24h, capped at 30 days)
await engine.uploadBlob('d:alex', bytes, { policy: 'transport' });
await engine.uploadBlob('g:acme', bytes, { policy: 'ephemeral', ttlMs: 3_600_000 });

Retention is intent, not enforcement. These policies are executed by a relay you may not control. A relay that keeps a copy is invisible to you.

A blob may predate a rotation, so downloadBlob tries the keyring newest-epoch first. uploadBlob throws if the relay already holds different bytes under a freshly minted ID — 96 random bits colliding means something is very wrong, and a record pointing at somebody else’s ciphertext is worse than an error.

Blob egress is limited by request count, not by bytes. Each member may make 120 blob requests per minute, and a blob may be 25 MB. Bandwidth shaping belongs in front of the relay.