Browser-direct upload
Three steps, and the middle one skips your server entirely.
- Server presigns. It holds the key; the browser never sees it.
- Browser PUTs the bytes straight to object storage.
- Server registers the upload and waits for processing.
/** * Browser-direct upload: the bytes never touch your server. * * Your server holds the key and signs the request; the browser PUTs straight to * object storage. This is the pattern you want for large files — a 400 MB video * that transits your API costs you the bandwidth twice and the memory once. */import { AquienpzClient } from "@aquienpz/sdk";
const nt = new AquienpzClient({ endpoint: process.env.NITIDA_ENDPOINT ?? "https://assets.example.com", apiKey: process.env.NITIDA_RUNTIME_KEY, tenantCode: "demo", tenantId: Number(process.env.NITIDA_TENANT_ID ?? 1),});
/** SERVER — step 1. Hand the browser a URL it may PUT to, and nothing else. */export async function startUpload(input: { sha256: string; mime: string; bytes: number; fileName: string;}) { const presign = await nt.assets.presignUploadUrl({ ...input, presets: ["original", "thumb", "lg"], });
// Those exact bytes already exist for this tenant: nothing needs to fly. if ("deduped" in presign && presign.deduped) return { deduped: true, presign };
return { deduped: false, presign };}
/** * SERVER — step 3, after the browser reports its PUT returned 200. * * Forward `presign.process.body` VERBATIM. It is deliberately opaque: it * carries the raw key, the presets and the video knobs, and rebuilding it by * hand is how the two halves drift apart. */export async function finishUpload(processBody: Record<string, unknown>) { return nt.assets.processAndWait(processBody, { timeoutMs: 300_000 });}
/** * BROWSER — step 2. * * ⚠️ Object storage answers this preflight ITSELF. If your origin is missing * from the BUCKET's CORS policy you get "network error" with every other step * green — and it cannot be fixed in this SDK, in your app, or in any tenant * setting. It is a bucket policy, and only a bucket policy. */export async function putFromBrowser(url: string, file: File) { const res = await fetch(url, { method: "PUT", body: file, headers: { "Content-Type": file.type }, }); if (!res.ok) throw new Error(`PUT failed: ${res.status}`);}The CORS trap
Section titled “The CORS trap”Step 2 does not talk to your API, so your CORS settings have no say in it. Object storage answers that preflight itself, from the bucket’s policy.
The symptom is unhelpful on purpose: PUT failed: network error, with
compression, hashing and presign all reporting success. Everything you control
looks fine, because the thing that rejected the request is not something you
control from the app.
Why process.body is opaque
Section titled “Why process.body is opaque”It carries the raw storage key, the preset ladder and the video knobs. Passing it through untouched keeps the two halves of the flow in sync; reconstructing it by hand is how they drift, and the drift shows up much later as a missing variant rather than as an error.
When the sha already exists
Section titled “When the sha already exists”presignUploadUrl short-circuits with { deduped: true, asset } when those
exact bytes are already stored for your tenant. No PUT, no processing, no
second copy — you already have the asset, so use it.
Worth knowing while testing: this is also why a retried upload can hide a bug in the non-deduped path. If you are verifying upload behaviour, generate genuinely unique bytes each run, or you will be exercising the dedup branch and concluding everything works.