# Uploads that survive a reload

> When `aq.upload()` is not enough, and the two packages that pick up where it stops — resume in the browser, and an upload that keeps going while the app is backgrounded.

`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

```bash
bun add @nitida/asset-uploader-web
```

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

## Expo

```bash
bunx expo install @nitida/asset-uploader-expo
```

The 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:

```json
{ "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

⭐ **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. `UploadSession` has 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

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