# How it works, end to end

> Where each step runs, what crosses which boundary, and which parts are yours versus ours.

The most common source of confusion is not the API — it is *where* each thing
happens. This page is the map.

## One upload, start to finish

```mermaid
sequenceDiagram
    autonumber
    participant D as Browser / phone
    participant S as Your server
    participant N as nitida
    participant R as Object storage
    participant C as CDN

    Note over D: compress in a worker,<br/>then sha256 — both on the device
    D->>S: ask for an upload URL (sha, mime, bytes)
    S->>N: presign  🔑 the only calls that need the key
    N-->>S: signed URL + dedup check
    S-->>D: signed URL
    Note over D,R: the bytes skip your server entirely
    D->>R: PUT the file
    R-->>D: 200
    D->>S: "the PUT succeeded"
    S->>N: register
    N->>N: variants · palette · (video: transcode, HLS)
    N-->>S: assetId + status
    N->>S: webhooks (optional)
    C-->>D: every <img> and <video> from here on
```

**What each boundary means:**

- **Compression and hashing are on the device.** A 12 MP photo becomes ~700 kB
  before a byte crosses the network.
- **The PUT skips your server entirely.** A 400 MB video does not transit your
  API, cost you bandwidth twice, or sit in your memory. **This is also why the
  *bucket's* CORS policy is the one that matters** — your app is not in that
  request, so no setting of yours can fix it.
- **Only presign and register need the key**, which is why they are on your
  server, and why the browser client's type refuses an `apiKey`.
- **Variants, palette, transcode and CDN are ours.**

## What is yours and what is ours

```mermaid
flowchart LR
    subgraph YOU["your app"]
        A["file picker,<br/>progress UI"]
        B["one route<br/>~15 lines"]
        E["what a palette<br/>or caption MEANS"]
    end
    subgraph SDK["the SDK"]
        C["compress · hash ·<br/>resumable transport ·<br/>URL builders"]
    end
    subgraph NT["nitida platform"]
        D["variants · palette ·<br/>transcode · HLS · CDN"]
    end
    A --> C --> B --> D --> E
```

That last box is the seam people ask about most. We produce the artifacts; what
they *mean* is your product, not ours — see
[Beyond images](/concepts/beyond-images/).

## Where compression runs, and how many at once

In a **worker**, on `OffscreenCanvas`, so the main thread never blocks —
scrolling stays smooth while a dozen photos encode.

Concurrency is adaptive and deliberately conservative:

| | value | why |
|---|---|---|
| CPU work concurrency | **2** | empirical sweet spot on an 8-core laptop |
| batch size, desktop | **4** | |
| batch size, **mobile** | **1** | sequential, to avoid OOM on low-end Android |
| core cap | **8** | respect `navigator.hardwareConcurrency`, never fan out to 32 workers on a big machine |

The mobile default is the one worth knowing: a phone running four
decode-resize-encode pipelines at once does not go faster, it gets killed.

## Variants versus transforms — two URL shapes

This trips up almost everyone once.

```mermaid
flowchart TD
    SHA["an asset = its SHA-256"]
    SHA --> V["STORED VARIANT<br/>pre-generated at upload<br/><code>/&lt;tenant b36&gt;/v/&lt;sha&gt;-lg.webp</code>"]
    SHA --> T["ON-THE-FLY TRANSFORM<br/>generated on first request<br/><code>/t/format=webp,width=640/&lt;sha&gt;.webp</code>"]
    V --> V2["tenant-scoped ✓<br/>use for: the standard rungs,<br/>and ALL video"]
    T --> T2["global, no tenant segment<br/>use for: a crop or width<br/>the ladder does not have"]
```

Transforms are content-addressed by SHA and resize from the source on demand,
so they need no tenant segment. Variants are pre-generated *per tenant* and do.

:::danger[A video is always a variant, never a transform]
`/t/` on a stored video never returns playable video: with a poster it answers
**200** with the transformed *poster frame*, without one it answers **410**. Use
`urlFor(asset, "video")`.
:::

## What happens to a file that is not an image

Four different paths, decided by MIME:

```mermaid
flowchart TD
    U["upload"] --> K{"MIME?"}
    K -->|"image/*"| I["resize ladder + palette<br/>→ WebP variants"]
    K -->|"video/*"| V["background job:<br/>poster · mp4 · HLS · aiproxy"]
    K -->|"audio/*"| A["transcode to MP3 inline<br/>+ keep the original"]
    K -->|"anything else"| P["passthrough:<br/>stored raw, served as-is"]
```

See [Documents, audio and everything else](/concepts/other-file-types/) for what
that last box does and does not give you.