Primitive Payloads: Streaming Large Attachments
Upload and download large, content-addressed, end-to-end-encrypted attachment objects in bounded memory, then attach them to email by reference instead of inlining bytes.
Use Primitive Payloads, the streaming client for large, content-addressed, end-to-end-encrypted attachment objects, when a file is too big to inline on send, reply, or forward. Inline attachments (base64 content) cover anything under the inline cap (~30 MiB of combined raw bytes); Payloads uploads and downloads in bounded memory instead.
A finalized Payloads object is identified by PushResult.merkleRoot, a 64-character lowercase-hex Merkle root, and decrypted with PushResult.cek, a hex content-encryption key the caller holds. Reference the object on a send instead of inlining its bytes.
For anything under the inline cap, skip Payloads entirely and pass attachments directly to client.send / client.reply / client.forward. Reach for pushFile / pushBytes only once you're above that threshold, or use client.sendAttachment, which picks inline-vs-reference for you based on size.
Upload and attach a large file#
Upload the file with pushFile, then deliver it by reference through payloadAttachments on client.send. Two steps, no inline bytes.
- 1
Push the file to Payloads storage#
Stream the file from disk with
pushFile(usepushBytesfor in-memory data instead). Both are exported from the payloads module of@primitivedotdev/sdk.// upload.ts import { pushFile } from "@primitivedotdev/sdk/payloads"; const pushed = await pushFile({ path: "./recording.mp4", apiKey: process.env.PRIMITIVE_API_KEY!, }); console.log(pushed.merkleRoot); // 64-char lowercase hex console.log(pushed.cek); // hex content-encryption keypushedis aPushResult:merkleRootis the content-addressed id for the finalized object, andcekis the hex content-encryption key the recipient needs to decrypt it. Keep both; you need them to reference or download the object later. - 2
Reference the uploaded object as an attachment#
Build a
SendPayloadReferencefrom the push result and pass it onsendviapayloadAttachmentsinstead ofattachments. Only one payload attachment is supported per send in v1.// send.ts import primitive from "@primitivedotdev/sdk"; const client = primitive.client({ apiKey: process.env.PRIMITIVE_API_KEY!, }); await client.send({ from: "Support <support@example.com>", to: "alice@example.com", subject: "Hello", bodyText: "Your recording is attached.", payloadAttachments: [ { root: pushed.merkleRoot, filename: "recording.mp4", contentType: "video/mp4", cek: pushed.cek, }, ], });The SDK converts
cek(hex, matchingPushResult.cekverbatim) to the base64url encoding the wire format expects, so the entire SDK surface, push, pull, and reference, stays hex to the caller.
Skip the manual push-then-reference dance with client.sendAttachment(...). Pass the normal send fields plus a single attachment (content for in-memory bytes, or path to stream a file from disk), and the SDK sends it inline when it's at or below inlineThreshold (defaults to 25 MiB, the server's inline/offload threshold) and uploads-then-references it otherwise. Provide exactly one of content or path.
Download a payload object#
Use pullFile to stream a Payloads object back down and decrypt it, passing the same merkleRoot and cek the push returned.
// download.ts
import { pullFile } from "@primitivedotdev/sdk/payloads";
await pullFile({
root: pushed.merkleRoot,
cek: pushed.cek,
path: "./downloaded-recording.mp4",
apiKey: process.env.PRIMITIVE_API_KEY!,
});
pullFile streams and decrypts in bounded memory, so a multi-gigabyte object never has to be resident.
The CEK is client-held: the SDK's push/pull/reference surface carries it, so if you lose the cek for a merkleRoot you cannot decrypt the object. Persist both together (for example alongside the email record) if you need to re-download later.
Next steps#
Send inline attachments directly for anything under the inline cap.
Receiving Inbound EmailSee how ReceivedEmail exposes attachments and download references.
Request Options and IdempotencyApply per-call timeouts and idempotency keys to send calls that reference payloads.
Payloads Command: Streaming Large Attachments from the CLIUpload and download the same encrypted objects from the terminal.
Was this page helpful?