Skip to content
scroll to zoom · drag to pan

How it works, end to end

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

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

Where compression runs, and how many at once

Section titled “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

Section titled “Variants versus transforms — two URL shapes”

This trips up almost everyone once.

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.

What happens to a file that is not an image

Section titled “What happens to a file that is not an image”

Four different paths, decided by MIME:

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 for what that last box does and does not give you.