# 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.

```ts
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

| 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) |

```ts
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.

---
