# Browser-direct upload

> Presign on the server, PUT from the browser, register on the server — without the bytes ever transiting your API.

import { Code } from '@astrojs/starlight/components';
import browserDirect from '../../../../examples/04-browser-direct-upload.ts?raw';

Three steps, and the middle one skips your server entirely.

1. **Server** presigns. It holds the key; the browser never sees it.
2. **Browser** PUTs the bytes straight to object storage.
3. **Server** registers the upload and waits for processing.

<Code code={browserDirect} lang="ts" title="browser-direct-upload.ts" />

## The CORS trap

Step 2 does not talk to your API, so *your* CORS settings have no say in it.
Object storage answers that preflight itself, from the **bucket's** policy.

The symptom is unhelpful on purpose: `PUT failed: network error`, with
compression, hashing and presign all reporting success. Everything you control
looks fine, because the thing that rejected the request is not something you
control from the app.

:::caution
Applying a CORS policy usually **replaces** the whole policy rather than adding
to it. List the current one and diff before you write, or you will fix your own
origin by removing everyone else's.
:::

## Why `process.body` is opaque

It carries the raw storage key, the preset ladder and the video knobs. Passing
it through untouched keeps the two halves of the flow in sync; reconstructing
it by hand is how they drift, and the drift shows up much later as a missing
variant rather than as an error.

## When the sha already exists

`presignUploadUrl` short-circuits with `{ deduped: true, asset }` when those
exact bytes are already stored for your tenant. No PUT, no processing, no
second copy — you already have the asset, so use it.

Worth knowing while testing: this is also why a **retried** upload can hide a
bug in the non-deduped path. If you are verifying upload behaviour, generate
genuinely unique bytes each run, or you will be exercising the dedup branch and
concluding everything works.