Uploads that survive a reload
aq.upload() sends a file in a single PUT. That is what almost every consumer
needs, and you should not reach past it without a reason.
The reason is resume. A single PUT is atomic in the worst way: interrupt it at 95% — a reload, a tunnel, an iOS app backgrounded by the user switching to Messages — and you start from zero. On a 2 GB video over a phone connection, that is not an inconvenience; it is a feature that does not work.
Two packages pick up there. Both are optional: install one only if the sentence above describes your product.
| package | resumes across | |
|---|---|---|
| Browser | @nitida/asset-uploader-web |
a page reload, a crashed tab, a closed laptop |
| Expo / React Native | @nitida/asset-uploader-expo |
the app being backgrounded or killed |
Browser
Section titled “Browser”bun add @nitida/asset-uploader-webimport { UploadTask } from "@nitida/asset-uploader-web";
const task = new UploadTask({ endpoint: process.env.NEXT_PUBLIC_NITIDA_URL!, tenantCode: "your-tenant", file, // Your OWN backend's token, not a nitida API key — see "The credential" below. authToken: await getSessionToken(), onProgress: ({ sentBytes, totalBytes }) => setPct(Math.round((sentBytes / totalBytes) * 100)),});
const { assetId, sha256 } = await task.start();Construct the same UploadTask again for the same file and it finds the
in-flight session and continues. It matches on (tenantCode, sha256), not on a
handle you have to keep — so a fresh page after a reload resumes without you
having stored anything.
bunx expo install @nitida/asset-uploader-expoThe JS API is deliberately the same shape, so a call site written for the web
reads identically. Underneath it is URLSession with a background
configuration on iOS and WorkManager on Android: the OS keeps uploading after
your process is gone, and hands the result back when the app returns.
It ships an Expo config plugin. Prebuild picks it up from the package root; you
do not add anything to app.json beyond the plugin entry:
{ "expo": { "plugins": ["@nitida/asset-uploader-expo"] } }The plugin appends UIKit’s background-URLSession event handler to your
Swift AppDelegate. Without it, an upload that finishes while the app is suspended
never fires its completion handler and the asset stays “uploading” forever —
which looks exactly like a hung server and is not one.
The credential, and what lands in the browser
Section titled “The credential, and what lands in the browser”⭐ An uploader never sees a nitida API key. authToken is your backend’s
token; the uploader forwards it to your endpoint, which presigns against
nitida with the key it holds server-side. If you find yourself putting an
amk_* key in authToken, stop: you are about to ship a platform credential
to every visitor.
The web uploader persists its session in IndexedDB, because that is what “resume after a reload” means. What is in there, stated so nobody has to discover it:
- The part URLs — presigned R2 PUT URLs. These are bearer credentials, and the bounds are the point: write-only, to the parts of one upload the user themselves started, 60-minute TTL, IndexedDB scoped to your origin, and the row is deleted on complete and on abort.
- Not the
authToken. It lives in memory on the task and is never written down.UploadSessionhas no field for it — that is the enforcement, not the intention. A reload therefore loses it and your app supplies it again.
When NOT to use these
Section titled “When NOT to use these”- Files that comfortably fit one PUT.
aq.upload()is fewer moving parts, and fewer moving parts is the whole argument. - A server-side script. There is no reload to survive; retry the PUT.
- “Just in case.” Both packages add real bundle weight and a native module on the Expo side. A feature nobody reaches is a cost everybody pays.