Skip to content
s3nd
Menu

Use case

Attachments beside the data

The blobs an app holds, carried across as objects, with the records that point at them in the snapshot.

// Each blob becomes an object; the record keeps the key.
const stored = await store.upload(blob, { filename: 'scan.pdf', prefix: `attachments/${code}` })
record.attachmentKey = stored.key

// The snapshot carries the records, not the bytes.
await store.putSnapshot(code, { records }, { expiresIn: 3600 })

// On the other device, fetch on demand.
const url = await store.getUrl(record.attachmentKey, { expiresIn: 600 })

The situation

What is actually going on.

The problem

IndexedDB stores Blobs natively, so local-first apps end up holding images, recordings and PDFs next to their records. Base64 inside a JSON snapshot inflates them by a third and defeats gzip: three ways to hit the size ceiling at once.

What s3nd does about it

Use the file API underneath the snapshots. Upload each blob as an object, keep its key in the record, and let the snapshot carry references. The receiving device fetches what it needs, when it needs it, through a presigned URL if you prefer the bytes never transit your server.

Worth watching

The things that are easy to get wrong.

Same prefix, same lifecycle

Put attachments under a prefix that shares the transfer’s expiry rule, or they outlive the snapshot they belonged to.

Streams need a length

A single PutObject cannot use chunked encoding. Pass contentLength for a stream; buffers and Blobs already know their size.

Packagess3nd

Put a file. Hand over the code.

Point it at the bucket you already pay for. Nothing to deploy, nothing to sign up for, nothing in the middle.

npm install -g @s3nd/clis3nd init --provider r2 --bucket drops3nd put ./anything.zip