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"Retention policies
Section titled “Retention policies”| 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.