# Mobile — React Native and Expo

> Native background uploads that survive the app being backgrounded, and compression that does not melt the phone.

The mobile path is not "the web path in a WebView". Two subpaths exist because
a phone has constraints a browser does not.

## Uploads that survive backgrounding

:::danger[This path puts a runtime key on the device]
`createExpoUploader` takes `apiKey` off the client and hands it to the native
session as its `authToken`. It has to: the OS replays that upload from a
background task, possibly hours later, and there is no server of yours in that
loop to inject a credential.

So the client below is built from `@nitida/sdk/server` **on the phone**, and an
`amk_rt_*` key ends up in your bundle — readable by anyone who unzips the IPA or
APK, and a **write** key to a paid platform.

It is the opposite call from [`@nitida/sdk/web`](/examples/browser-direct/),
which met the same constraint and chose not to expose its multipart uploader at
all. Background survival is the only reason to accept it.

**If it is not worth it:** build the client from `@nitida/sdk/web` pointed at
your own route and call `aq.upload(file)`. No key on the device. You lose
survival across backgrounding and OS kill — the upload dies with the JS thread.

**Scope the key to one tenant** so a leak is contained, and know that rotating
it is your revocation. A BFF-minted short-lived token the native session could
carry is not built yet.
:::

```ts
import { NitidaClient } from "@nitida/sdk/server";
import { createExpoUploader } from "@nitida/sdk/expo";

const task = createExpoUploader(client, {
  uri: asset.uri,          // from expo-image-picker
  fileName: "photo.jpg",
  partSize: 8 * 1024 * 1024,
  concurrency: 2,
});
```

`createExpoUploader` inherits the client's endpoint, key and tenant, so the
call site only says what is being uploaded and how hard to push.

The transfer is delegated to a **native background session** — `URLSession` on
iOS, `WorkManager` on Android. That matters because the upload then survives:

- the user backgrounding the app,
- the screen locking,
- the OS suspending your JS runtime.

A JS-driven upload survives none of those. On a 100 MB video over cellular, that
is the difference between a feature and a support ticket.

:::caution[The key still cannot live in the app]
A mobile binary is not a secret. Point the client at **your** backend and let it
presign, exactly like the browser. `@nitida/sdk/expo` reads the client's config;
it does not make a client key safe.

For a BFF that authenticates by session, Expo cannot send cookies automatically
— pass them explicitly via the client's `headers`.
:::

## Compression on the device

```ts
import { compressImage } from "@nitida/sdk/native";
```

A thin pass-through to the native compressor, with the **same JS API as the web
one**, so call sites are identical across web and React Native. That is the
whole design goal: shared upload code, different engine underneath.

### The concurrency default is 1, and leave it there

Mobile batches **sequentially**. A phone running four decode-resize-encode
pipelines at once does not finish sooner — low-end Android runs out of memory
and the OS kills the process. Desktop defaults to 4 for the same reason
inverted.

## HEIC is the normal case here

iPhones produce HEIC by default. It decodes **natively** in ~136 ms where the
engine supports it, falling back to a 341 kB WASM decoder only where it does
not — so most users never fetch the decoder.

Worth confirming on a real device rather than a simulator: the simulator's
engine is not the phone's.