Skip to content

Part 9 — blobs

Records are capped at 512KB, and last-write-wins on a 20MB payload is a bad idea. Large content goes in a blob, sealed the same way, addressed by an id a record points at.

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. 120 blob requests a minute per member, and a blob may be 25MB. Bandwidth shaping belongs in front of the relay.