# Presigned uploads (/docs/guides/presigned-uploads)



With presigned uploads, your server never handles file bytes. It only signs requests,
which keeps it fast, cheap, and free of body-size limits.

<Sequence
  label="Presigned upload sequence"
  actors="[&#x22;Browser&#x22;, &#x22;Your route&#x22;, &#x22;Bucket&#x22;]"
  messages="[
  { from: 0, to: 0, label: &#x22;Select and validate&#x22; },
  { from: 0, to: 1, label: &#x22;presign { name, type, size }&#x22;, note: &#x22;auth · limits · key&#x22; },
  { from: 1, to: 0, label: &#x22;Signed PUT URL&#x22; },
  { from: 0, to: 2, label: &#x22;PUT bytes&#x22;, note: &#x22;with progress, straight to storage&#x22; },
  { from: 0, to: 1, label: &#x22;complete { key }&#x22;, note: &#x22;HEAD to verify · save metadata&#x22; },
  { from: 1, to: 0, label: &#x22;{ key, url, data }&#x22; },
]"
/>

## What the server enforces [#what-the-server-enforces]

Client-side validation is a convenience; the route is the source of truth.

* **Authentication and authorization**: `authorize` runs before every action.
* **Size**: `maxFileSize` is checked before signing, and the signature locks the
  `Content-Length`, so S3 rejects a larger body.
* **Type**: `allowedTypes` is checked before signing, and the signature locks the
  `Content-Type`.
* **Existence**: `complete` verifies the object with `HEAD` before calling
  `onUploadComplete`, so clients can't register files they never uploaded.
* **Keys**: generated on the server (`getKey`), never trusted from the client.

## Single PUT or multipart? [#single-put-or-multipart]

|                | Single PUT      | Multipart                    |
| -------------- | --------------- | ---------------------------- |
| Max size       | 5 GB (S3)       | 5 TB                         |
| Pause / resume | Restarts from 0 | Continues from the last part |
| Parallelism    | One stream      | Several parts at once        |
| Requests       | 3               | 2 + parts × 2                |

`s3Adapter` picks automatically with `multipart.threshold` (64 MB by default).

## Next.js [#nextjs]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npx shadcn@latest add @uploadcn/upload-route
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add @uploadcn/upload-route
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn dlx shadcn@latest add @uploadcn/upload-route
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun x shadcn@latest add @uploadcn/upload-route
    ```
  </CodeBlockTab>
</CodeBlockTabs>

See [Amazon S3](/docs/adapters/s3) and [Cloudflare R2](/docs/adapters/r2) for the full
setup, and [TanStack Start](/docs/frameworks/tanstack-start) for other frameworks.
