# Migrating from v1 to v3

v1 shipped as a single React package, `upup-react-file-uploader`. v3 is a
ground-up rewrite: a framework-agnostic headless core (`@useupup/core`) with a
native UI for **React, Vue, Svelte, Angular, Vanilla JS, and Preact**, an
optional server package (`@useupup/server`) for signed uploads and
server-proxied cloud drives, and a Next.js package (`@useupup/next`). The React
component keeps the same name — `UpupUploader` — and the same idea, but **most
props were renamed or restructured**. This is a major upgrade, not a drop-in
bump; budget time to sweep your props.

Rough time budget: **1–2 hours** for a typical single-`UpupUploader` app,
longer if you customized styling via `classNames` or built programmatic upload
control on the ref API.

## What changed at a glance

- **Package rename.** `upup-react-file-uploader` → `@useupup/react`. The
  `/styles` and `/server` subpaths move to `@useupup/react/styles` and the
  standalone `@useupup/server` package.
- **Six frameworks.** The React UI is the canon; `@useupup/vue`,
  `@useupup/svelte`, `@useupup/angular`, `@useupup/vanilla`, and
  `@useupup/preact` render the same DOM.
- **Headless core.** `@useupup/core` holds the engine, the error taxonomy, i18n
  bundles, and theme contracts. You can build a fully custom UI on it.
- **Two upload modes.** v1's single `tokenEndpoint` becomes either
  `uploadEndpoint` (**client mode** — your route signs URLs, the browser uploads
  directly) or `mode="server"` + `serverUrl` (**server mode** —
  `@useupup/server` signs the URLs, holds credentials, and proxies drives).
- **Prop renames.** `limit` → `maxFiles`, `accept` → `allowedFileTypes`, `dark`
  → `theme.mode`, `classNames` → `theme.slots`, `uploadAdapters` → `sources`,
  `driveConfigs` → `cloudDrives`, `customProps` → `metadata`,
  `enableAutoCorsConfig` → `cors`, `localePack`/`translations` → `i18n`. Full
  table below.
- **Error taxonomy.** The `UploadError` / `UploadErrorType` pair becomes
  `UpupError` + the `UpupErrorCode` enum (with typed subclasses), exported from
  `@useupup/core`.

## Install

```sh
npm uninstall upup-react-file-uploader
npm i @useupup/react
```

Add `@useupup/server` only if you adopt server mode:

```sh
npm i @useupup/server
```

If you catch upload errors by type (see [Error handling](#error-handling)), also
add the core package so you can import the error classes directly:

```sh
npm i @useupup/core
```

Then update your imports:

```diff
- import { UpupUploader, UpupProvider } from 'upup-react-file-uploader'
- import 'upup-react-file-uploader/styles'
+ import { UpupUploader } from '@useupup/react'
+ import '@useupup/react/styles'
```

<Callout type="note">
    `UpupProvider` and `UploadAdapter` are **gone** as exports. `provider` is
    now a plain string (`"aws"`), and upload methods are configured with
    `sources` (string ids), not the `UploadAdapter` enum. If you need the
    storage-provider type, `@useupup/react` re-exports `StorageProvider` from
    `@useupup/core`.
</Callout>

## Props: v1 → v3

Every v1 `UpupUploader` prop, and where it went in v3. Exact v1 names are on the
left; exact v3 names on the right.

| v1 prop                                        | v3 prop                                                                               | Notes                                                                                                                                                                                                                                                                 |
| ---------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider={UpupProvider.AWS}` (required)       | `provider="aws"` (optional)                                                           | Now a lowercase string, not the `UpupProvider` enum. Enum values are unchanged (`aws`, `azure`, `backblaze`, `digitalocean`) and v3 adds many more S3-compatible ids (`r2`, `wasabi`, `minio`, `gcs`, …). No longer required — omit it for local-only file selection. |
| `tokenEndpoint="/api/upload-token"` (required) | `uploadEndpoint="/api/upload-token"` **or** `mode="server"` + `serverUrl="/api/upup"` | Renamed and split into client mode vs server mode. See [Storage & upload path](#storage--upload-path).                                                                                                                                                                |
| `accept="image/png"`                           | `allowedFileTypes="image/png"`                                                        | Renamed. Accepts a string, a `string[]`, or preset names (e.g. `"images"`, `"documents"`).                                                                                                                                                                            |
| `dark={true}`                                  | `theme={{ mode: 'dark' }}`                                                            | `theme.mode` accepts `'light'`, `'dark'`, or `'system'`.                                                                                                                                                                                                              |
| `classNames={{ … }}` (flat map)                | `theme={{ slots: { … } }}` (nested)                                                   | The flat keys (`adapterButton`, `progressBarInner`, …) become nested slot paths — see the [theming guide](/docs/guides/theming/) for the full slot structure.                                                                                                         |
| `limit={5}`                                    | `maxFiles={5}`                                                                        | Renamed. **Default changed from `1` to `10`** — see [Gotchas](#gotchas).                                                                                                                                                                                              |
| `mini={true}`                                  | `mini={true}`                                                                         | Unchanged.                                                                                                                                                                                                                                                            |
| `maxFileSize={{ size: 20, unit: 'MB' }}`       | `maxFileSize={{ size: 20, unit: 'MB' }}`                                              | Unchanged shape. v3 adds `minFileSize` and `maxTotalFileSize` (same object shape).                                                                                                                                                                                    |
| `maxRetries={3}`                               | `maxRetries={3}`                                                                      | Unchanged.                                                                                                                                                                                                                                                            |
| `resumable={{ mode: 'multipart' }}`            | `resumable={{ protocol: 'multipart' }}`                                               | The key `mode` was renamed to `protocol`. v3 also supports `{ protocol: 'tus', endpoint }`.                                                                                                                                                                           |
| `uploadAdapters={[UploadAdapter.INTERNAL, …]}` | `sources={['local', …]}`                                                              | Enum array → string-id array. Mapping below.                                                                                                                                                                                                                          |
| `driveConfigs={{ … }}`                         | `cloudDrives={{ … }}`                                                                 | Renamed, keys are camelCased. See [Sources & cloud drives](#sources--cloud-drives).                                                                                                                                                                                   |
| `imageEditor={true}`                           | `imageEditor={true}`                                                                  | Unchanged (`boolean \| ImageEditorOptions`). React/Preact only.                                                                                                                                                                                                       |
| `localePack={fr_FR}`                           | `i18n={{ locale: frFR }}`                                                             | Locale bundles now live under `i18n`. v1 exported snake_case bundles; v3's are camelCase, imported from `@useupup/core` (`enUS`, `frFR`, `arSA`, `deDE`, `esES`, `jaJP`, `koKR`, `zhCN`, `zhTW`).                                                                     |
| `translations={{ browseFiles: '…' }}`          | `i18n={{ overrides: { … } }}`                                                         | Per-key overrides move under `i18n.overrides`, and are **namespaced** (e.g. `{ fileList: { uploadFiles: '…' } }`) rather than flat.                                                                                                                                   |
| `customProps={{ … }}`                          | `metadata={{ … }}`                                                                    | Renamed. Still forwarded to your upload route.                                                                                                                                                                                                                        |
| `enableAutoCorsConfig={true}`                  | `cors={{ dangerouslyAutoConfigure: true, allowedOrigins: [...] }}`                    | Replaced by the `cors` object. Auto-configuration is now explicitly opt-in and named `dangerouslyAutoConfigure`.                                                                                                                                                      |
| `shouldCompress={true}`                        | `imageCompression={true}`                                                             | Renamed (boolean → boolean). Same semantics.                                                                                                                                                                                                                          |
| `showSelectFolderButton={true}`                | `folderUpload={{ showSelectFolderButton: true }}`                                     | Moved under the `folderUpload` object.                                                                                                                                                                                                                                |
| `allowPreview={true}`                          | `allowPreview={true}`                                                                 | Unchanged.                                                                                                                                                                                                                                                            |
| `isProcessing={busy}`                          | `isProcessing={busy}`                                                                 | Unchanged.                                                                                                                                                                                                                                                            |
| `icons={{ … }}`                                | `icons={{ … }}`                                                                       | Unchanged (the per-framework component types differ).                                                                                                                                                                                                                 |

v3 also adds many new props with no v1 equivalent — among them `autoUpload`,
`thumbnailGenerator`, `heicConversion`, `stripExifData`, `checksumVerification`,
`contentDeduplication`, `crashRecovery`, `webWorker`, `maxConcurrentUploads`,
`enablePaste`, and `processingEndpoint`. See the
[React quickstart](/docs/quickstarts/react/) for the modern surface.

## Sources & cloud drives

`uploadAdapters` (an `UploadAdapter` enum array) becomes `sources` (a string-id
array). The order still controls tab order.

```diff
- import { UpupUploader, UploadAdapter } from 'upup-react-file-uploader'
- <UpupUploader
-   uploadAdapters={[UploadAdapter.INTERNAL, UploadAdapter.GOOGLE_DRIVE]}
- />
+ import { UpupUploader } from '@useupup/react'
+ <UpupUploader sources={['local', 'googleDrive']} />
```

Adapter → source id mapping:

| v1 `UploadAdapter` | v3 `sources` id                     |
| ------------------ | ----------------------------------- |
| `INTERNAL`         | `'local'`                           |
| `GOOGLE_DRIVE`     | `'googleDrive'`                     |
| `ONE_DRIVE`        | `'oneDrive'`                        |
| `DROPBOX`          | `'dropbox'`                         |
| `LINK`             | `'url'`                             |
| `CAMERA`           | `'camera'`                          |
| _(new)_            | `'box'`, `'screen'`, `'microphone'` |

<Callout
    type="warning"
    title="v3's default sources add camera, microphone, and screen recording"
>
    v1's default surface was effectively local files plus link imports. v3
    defaults `sources` to `['local', 'url', 'camera', 'microphone', 'screen']`,
    so an uploader migrated **without** a `sources` prop silently grows three
    capture panels. Opening one asks the user for camera or microphone
    permission, and what it produces is a recording: `video/webm` for screen,
    `audio/webm` for microphone, `image/jpeg` for a camera photo. The exact
    recording type comes from the browser's `MediaRecorder` and may carry a
    `;codecs=…` suffix, so match it with a `video/*`-style wildcard rather than
    an exact string. If those types are not in your `allowedFileTypes` (or your
    server's `allowedTypes`), a user can record a clip and then watch it be
    rejected — a dead end with no way forward.
</Callout>

Pass `sources` explicitly to keep the v1 surface:

```tsx
<UpupUploader sources={['local', 'url']} />
```

`driveConfigs` becomes `cloudDrives`, and the snake_case keys become camelCase:

```diff
- driveConfigs={{
-   googleDrive: {
-     google_client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
-     google_api_key: process.env.NEXT_PUBLIC_GOOGLE_API_KEY!,
-     google_app_id: process.env.NEXT_PUBLIC_GOOGLE_APP_ID!,
-   },
-   oneDrive: {
-     onedrive_client_id: process.env.NEXT_PUBLIC_ONEDRIVE_CLIENT_ID!,
-   },
- }}
+ cloudDrives={{
+   googleDrive: {
+     clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
+     apiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY!,
+     appId: process.env.NEXT_PUBLIC_GOOGLE_APP_ID!,
+   },
+   oneDrive: {
+     clientId: process.env.NEXT_PUBLIC_ONEDRIVE_CLIENT_ID!,
+   },
+ }}
```

`oneDrive`, `dropbox`, and `box` each take `{ clientId, redirectUri? }`;
`googleDrive` takes `{ clientId, apiKey, appId }`.

## Theming & i18n

Two v1 props (`dark`, `classNames`) collapse into one `theme` object, and two
i18n props (`localePack`, `translations`) collapse into one `i18n` object.

```diff
  <UpupUploader
-   dark={isDark}
-   classNames={{ progressBarInner: 'my-fill', adapterButton: 'my-btn' }}
-   localePack={fr_FR}
-   translations={{ browseFiles: 'Send' }}
+   theme={{
+     mode: isDark ? 'dark' : 'light',
+     slots: {
+       progressBar: { fill: 'my-fill' },
+       sourceSelector: { sourceButton: 'my-btn' },
+     },
+   }}
+   i18n={{
+     locale: frFR,
+     overrides: { fileList: { uploadFiles: 'Send' } },
+   }}
  />
```

The flat `classNames` keys map one-to-one to nested slot paths; the
[theming guide](/docs/guides/theming/) documents the full slot structure. For
app-wide theming you can also wrap your tree in `UpupThemeProvider` (exported
from `@useupup/react`).

## Events

Most handlers keep their names and shapes. Two are worth a closer look:

| v1 event                | v3 event                | Change                                                                                                                                                                                                                                                   |
| ----------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onFileRemove`          | `onFileRemoved`         | Renamed to past tense — the old spelling is gone, not aliased.                                                                                                                                                                                           |
| `onFilesUploadComplete` | `onFilesUploadComplete` | Name unchanged; item type changed. Its argument is a list of files in both versions; v1 typed it `FileWithParams[]` (and v1's own docs mislabeled the items as storage "keys"), v3 types it `UploadFile[]` — the same type swap every file handler gets. |

Unchanged names: `onFilesSelected`, `onFileClick`, `onFileTypeMismatch`,
`onFileUploadComplete`, `onFileUploadStart`, `onFileUploadProgress`,
`onFilesDragOver`, `onFilesDragLeave`, `onFilesDrop`, `onFilesUploadProgress`,
`onIntegrationClick`, `onPrepareFiles`, `onDoneClicked`, `onWarn`, and
`onError`.

New in v3: `onUploadStart`, `onUploadComplete`, `onStatusChange`,
`onRestrictionFailed`, `onBeforeFileAdded`, and `onFileProcessed`.

<Callout type="note">
    `onFilesSelected`, `onFileClick`, and the per-file complete/start handlers
    now receive v3 `UploadFile` objects (which carry `id`, `key`, `status`, and
    the underlying `File`) instead of v1's `FileWithParams`. Property access
    like `file.name` and `file.type` still works.
</Callout>

## Ref API (programmatic control)

The ref pattern survives with a renamed type. `UpupUploaderRef` becomes
`UploaderRef`, and its `useUpload()` method returns a **superset** of the v1
shape:

```diff
- import { UpupUploader, UpupUploaderRef } from 'upup-react-file-uploader'
- const ref = useRef<UpupUploaderRef | null>(null)
+ import { UpupUploader, type UploaderRef } from '@useupup/react'
+ const ref = useRef<UploaderRef | null>(null)

  // ref.current.useUpload() still returns { files, loading, progress, upload, error }
  // v3 adds: resetState, uploadFiles, setFiles, replaceFiles
```

v3 also introduces a first-class headless hook, `useUpupUpload`, which is the
recommended way to drive uploads from your own UI. It returns reactive `files`,
`status`, `progress`, and `error`, plus `addFiles`, `upload`, `pause`,
`resume`, `cancel`, `retry`, and an `on(event, handler)` subscription — no ref
polling required.

```tsx
import { useUpupUpload } from '@useupup/react'

const { files, status, progress, error, addFiles, upload } = useUpupUpload({
    provider: 'aws',
    uploadEndpoint: '/api/upload-token',
})
```

## Error handling

v1 threw `UploadError` carrying an `UploadErrorType` enum. v3 replaces it with
`UpupError` (base class) and typed subclasses, keyed by the `UpupErrorCode`
enum. Both are exported from `@useupup/core`.

```diff
- import { UploadError, UploadErrorType } from 'upup-react-file-uploader'
+ import { UpupError, UpupErrorCode } from '@useupup/core'

  try {
    await doUpload()
  } catch (e) {
-   if (e instanceof UploadError && e.type === UploadErrorType.EXPIRED_URL) {
+   if (e instanceof UpupError && e.code === UpupErrorCode.PRESIGN_FAILED) {
      // handle re-signing
    }
  }
```

Key differences:

- The discriminator moved from `error.type` (an `UploadErrorType` value) to
  `error.code` (an `UpupErrorCode` value, a string). `retryable` and `status`
  are still present.
- v3 ships typed subclasses you can narrow on: `UpupAuthError`,
  `UpupNetworkError`, `UpupValidationError`, `UpupQuotaError`,
  `UpupStorageError`, and `UpupConfigError`.
- `UpupErrorCode` is a broader, more specific set than v1's eight types — e.g.
  `FILE_TOO_LARGE`, `TYPE_MISMATCH`, `LIMIT_EXCEEDED`, `PRESIGN_FAILED`,
  `CORS_ERROR`, `AUTH_EXPIRED`, `AUTH_DENIED`, `AUTH_REQUIRED`, `NETWORK_ERROR`,
  `UPLOAD_FAILED`, `QUOTA_EXCEEDED`, `STORAGE_ERROR`.

The simple `onError={(message) => …}` prop is unchanged (it still receives a
string), and the reactive `error` from `useUpupUpload()` is now a typed
`UpupError` rather than an opaque value. The ref's `useUpload().error` stays the
plain message string it was in v1.

## Storage & upload path

v1 had one path: a `tokenEndpoint` you implemented server-side with
`s3GeneratePresignedUrl` (from `upup-react-file-uploader/server`). The browser
received a presigned URL and uploaded bytes directly to storage. v3 keeps that
model as **client mode** and adds a **server-mode** option that holds your
credentials and proxies drive transfers.

### Client mode (closest to v1)

Rename `tokenEndpoint` to `uploadEndpoint`. Your route still returns a presigned
URL per file.

```diff
- <UpupUploader provider={UpupProvider.AWS} tokenEndpoint="/api/upload-token" />
+ <UpupUploader provider="aws" uploadEndpoint="/api/upload-token" />
```

You can keep hand-rolling that route, or adopt `@useupup/server`'s handler
(below), which implements presign, multipart, and drive OAuth for you. The v1
server helpers (`s3GeneratePresignedUrl`, the `s3*MultipartUpload` family,
`azureGenerateSasUrl`) are superseded by the handler's `/presign` and
`/multipart/*` routes.

### Server mode (new)

Point the uploader at a `@useupup/server` route and let it hold the credentials:

```tsx
<UpupUploader provider="aws" mode="server" serverUrl="/api/upup" />
```

```ts
import { createUpupHandler, InMemoryTokenStore } from '@useupup/server'

const handler = createUpupHandler({
    storage: {
        type: 'aws',
        bucket: process.env.S3_BUCKET!,
        region: process.env.S3_REGION!,
    },
    // Required, server-only: a stable, high-entropy secret (min 16 chars),
    // shared across every server instance. createUpupHandler throws without it.
    uploadTokenSecret: process.env.UPUP_UPLOAD_TOKEN_SECRET!,
    // OAuth client SECRETS live here, never in the browser:
    providers: {
        googleDrive: { clientId: '…', clientSecret: '…' },
    },
    tokenStore: new InMemoryTokenStore(),
    getUserId: async req => resolveUser(req),
})

export const GET = handler
export const POST = handler
```

Server mode is **secure-by-default**: `/presign` and `/multipart/init` return
`403 AUTH_REQUIRED` unless you configure `auth`, `getUserId`, or the explicit
`allowAnonymousUploads: true`. Every upload is bound to an HMAC-signed token
(key + uploadId + size), so a leaked presigned URL cannot be replayed for a
different object or a larger body.

<Callout type="info" title="Security upgrade">
- **v1's client-direct model:** cloud-drive integration was client-side only —
  the Google Drive `clientId`/`apiKey` and OneDrive `clientId` were public
  identifiers shipped to the browser (the normal client-side OAuth model;
  those are not secrets).
- **What v1 couldn't do:** keep the OAuth **client secret** and the drive
  access tokens off the client.
- **What v3 server mode adds:** the client talks to your `serverUrl` for OAuth
  and drive access, and `@useupup/server` performs the OAuth exchange and
  stores tokens in your `TokenStore`. It also proxies cloud-drive transfers —
  the server fetches the drive file and writes it to storage itself, so drive
  access tokens never reach the browser.
- **What stays the same for storage:** both modes still upload file bytes
  directly to storage via a presigned URL. In server mode, `@useupup/server`
  issues that URL — holding the storage credentials server-side exactly as
  your v1 `tokenEndpoint` did — and binds it to an HMAC token (a key +
  uploadId + size envelope).

Move to server mode when you need credential isolation, per-user token
scoping, or server-side scanning/compliance. Client mode (`uploadEndpoint`)
remains a valid, first-class choice.

</Callout>

<Callout type="note">
    `@useupup/server` speaks the S3 API only — set `storage.endpoint` for any
    non-AWS S3-compatible backend (MinIO, R2, DO Spaces, …).
    `StorageProvider.Azure` has no S3 surface, so `createUpupHandler` rejects it
    at construction time; use client mode with your own signing route for Azure.
</Callout>

See [Server Mode — Setup](/docs/guides/server-mode-setup/) for the Next.js,
Express, Fastify, and Hono adapters, and [Upload modes](/docs/guides/modes/) for
choosing between them.

### Re-check upload limits on the server

<Callout type="danger" title="v1 server-side maxFileSize/accept validation silently stops applying">
    v1 clients sent the uploader's configured constraints along with the presign
    request, so a v1 route could read them back and enforce them. v3 does not:
    the presign body is `{ name, type, size, metadata }` and carries **no
    constraints at all**. A v1 route ported field-for-field keeps answering
    `200` while validating nothing — the limits it reads off the body are now
    `undefined`, and every guard they feed passes. Nothing fails loudly, so the
    gap usually surfaces only when a billing or abuse cap turns out never to
    have been enforced.
</Callout>

`maxFiles`, `maxFileSize`, and `allowedFileTypes` are **client-side UX props**:
they keep the picker honest, and that is all they do in either version. Anyone
can POST to your presign route directly, so every limit you actually rely on has
to be re-checked where the URL is signed.

With `@useupup/server`, flat policy is declarative and per-user policy goes in the
`onBeforeUpload` hook. Both run on `/presign` and `/multipart/init` before any
storage call:

```ts
import { createUpupHandler } from '@useupup/server'

const handler = createUpupHandler({
    storage: {
        type: 'aws',
        bucket: process.env.S3_BUCKET!,
        region: process.env.S3_REGION!,
    },
    uploadTokenSecret: process.env.UPUP_UPLOAD_TOKEN_SECRET!,
    getUserId: async req => resolveUser(req),

    // Flat policy: 413 over the size cap, 415 on a disallowed type.
    maxFileSize: 25 * 1024 * 1024, // bytes — not the { size, unit } prop shape
    allowedTypes: ['image/*', 'application/pdf'],

    hooks: {
        // Per-user policy. `file` is { name, size, type }; `req` is the raw
        // Request. Returning false rejects with 403 "Upload rejected".
        onBeforeUpload: async (file, req) => {
            const plan = await resolvePlan(req)
            if (file.size > plan.perFileByteCap) return false
            if (plan.tier === 'free' && !file.type.startsWith('image/'))
                return false
            return await withinMonthlyQuota(plan, file.size)
        },
    },
})
```

If you kept a hand-rolled client-mode route instead, do the same work there: read
`name`, `type`, and `size` off the request body, reject before signing, and sign
for exactly the size you approved. The presigned `PUT` signature covers
`content-length`, so a client that later sends a bigger body is rejected by
storage rather than quietly landing an oversized object.

<Callout type="note">
    The server's `maxFileSize` is a **number of bytes**, not the
    `{ size, unit }` object the `<UpupUploader>` prop takes. They are separate
    settings that happen to share a name — one gates the picker, the other gates
    the signature.
</Callout>

See [Server Auth & Trust Model](/docs/guides/server-auth/#metadata-policy-size-and-type)
for the full policy surface, and
[`POST /presign`](/docs/api-reference/server-http/#post-presign) for the exact
request shape and status codes.

## Full example: before and after

**v1** (`upup-react-file-uploader`):

```tsx
'use client'

import {
    UpupUploader,
    UpupProvider,
    UploadAdapter,
} from 'upup-react-file-uploader'
import 'upup-react-file-uploader/styles'

export default function Uploader() {
    return (
        <UpupUploader
            provider={UpupProvider.AWS}
            tokenEndpoint="/api/upload-token"
            limit={5}
            accept="image/*"
            dark
            maxFileSize={{ size: 20, unit: 'MB' }}
            resumable={{ mode: 'multipart' }}
            uploadAdapters={[
                UploadAdapter.INTERNAL,
                UploadAdapter.GOOGLE_DRIVE,
            ]}
            driveConfigs={{
                googleDrive: {
                    google_client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
                    google_api_key: process.env.NEXT_PUBLIC_GOOGLE_API_KEY!,
                    google_app_id: process.env.NEXT_PUBLIC_GOOGLE_APP_ID!,
                },
            }}
            customProps={{ folder: 'avatars' }}
            onFileRemove={file => console.log('removed', file)}
            onFilesUploadComplete={files => console.log('done', files)}
        />
    )
}
```

**v3** (`@useupup/react`):

```tsx
'use client'

import { UpupUploader } from '@useupup/react'
import '@useupup/react/styles'

export default function Uploader() {
    return (
        <UpupUploader
            provider="aws"
            uploadEndpoint="/api/upload-token"
            maxFiles={5}
            allowedFileTypes="image/*"
            theme={{ mode: 'dark' }}
            maxFileSize={{ size: 20, unit: 'MB' }}
            resumable={{ protocol: 'multipart' }}
            sources={['local', 'googleDrive']}
            cloudDrives={{
                googleDrive: {
                    clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
                    apiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY!,
                    appId: process.env.NEXT_PUBLIC_GOOGLE_APP_ID!,
                },
            }}
            metadata={{ folder: 'avatars' }}
            onFileRemoved={file => console.log('removed', file)}
            onFilesUploadComplete={files => console.log('done', files)}
        />
    )
}
```

## Gotchas

Behavioral differences to check after the mechanical rename:

- **Your v1 server-side size/type validation silently stops applying.** v3's
  presign body carries no constraints, so a ported v1 route keeps returning
  `200` while checking nothing. Re-enforce the limits where the URL is signed —
  see [Re-check upload limits on the server](#re-check-upload-limits-on-the-server).
- **`sources` defaults to five sources, including camera, microphone, and
  screen recording.** Omit the prop and your app gains capture panels it never
  had in v1 — and recordings your `allowedFileTypes` may reject. Pass
  `sources={['local', 'url']}` for the v1 surface; see
  [Sources & cloud drives](#sources--cloud-drives).
- **`maxFiles` defaults to 10, not 1.** v1's `limit` defaulted to `1`. If your
  app relied on single-file selection, set `maxFiles={1}` (or use `mini`, which
  forces a single file).
- **`onFilesUploadComplete` always received file objects, not keys** — read
  `file.key` for the storage key.
- **`resumable.mode` is now `resumable.protocol`.** A leftover
  `{ mode: 'multipart' }` will not enable resumable uploads.
- **`onFileRemove` is now `onFileRemoved`.** The old spelling is gone (not
  aliased), so it silently stops firing if you miss it.
- **The panel is fixed-height by design.** The full uploader is 480px tall
  (max-width 600px); `mini` is a compact square (max-width 280px). Media views
  (camera, screen capture, previews) adapt to that box — the panel does not grow
  to fit content. This is unchanged from v1's sizing model.
- **`UpupProvider` and `UploadAdapter` no longer exist.** Replace enum usages
  with the string `provider` value and `sources` ids respectively.
- **A fresh core per mount.** v3 creates and `destroy()`s its engine on
  mount/unmount; hold state in your own app or via `useUpupUpload`, not across
  a remount of `<UpupUploader>`.

## Next steps

- [React quickstart](/docs/quickstarts/react/) — the modern v3 surface end to end.
- [Getting started](/docs/getting-started/) — local collection, client uploads,
  and server uploads in one page.
- [Server Mode — Setup](/docs/guides/server-mode-setup/) — adapters, auth, and
  production token stores.
- [Upload modes](/docs/guides/modes/) — client vs server, and when to pick each.
- Other frameworks: [Vue](/docs/quickstarts/vue/), [Svelte](/docs/quickstarts/svelte/),
  [Angular](/docs/quickstarts/angular/), [Vanilla JS](/docs/quickstarts/vanilla/),
  [Preact](/docs/quickstarts/preact/), [Next.js](/docs/quickstarts/next/).
