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/stylesand/serversubpaths move to@useupup/react/stylesand the standalone@useupup/serverpackage. - Six frameworks. The React UI is the canon;
@useupup/vue,@useupup/svelte,@useupup/angular,@useupup/vanilla, and@useupup/preactrender the same DOM. - Headless core.
@useupup/coreholds 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
tokenEndpointbecomes eitheruploadEndpoint(client mode — your route signs URLs, the browser uploads directly) ormode="server"+serverUrl(server mode — the browser talks only to@useupup/server, which 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/UploadErrorTypepair becomesUpupError+ theUpupErrorCodeenum (with typed subclasses), exported from@useupup/core.
Install
npm uninstall upup-react-file-uploader
npm i @useupup/reactAdd @useupup/server only if you adopt server mode:
npm i @useupup/serverIf you catch upload errors by type (see Error handling), also add the core package so you can import the error classes directly:
npm i @useupup/coreThen update your imports:
- import { UpupUploader, UpupProvider } from 'upup-react-file-uploader'
- import 'upup-react-file-uploader/styles'
+ import { UpupUploader } from '@useupup/react'
+ import '@useupup/react/styles'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.
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. |
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 for the full slot structure. |
limit={5} | maxFiles={5} | Renamed. Default changed from 1 to 10 — see 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. |
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 for the modern surface.
Sources & cloud drives
uploadAdapters (an UploadAdapter enum array) becomes sources (a string-id
array). The order still controls tab order.
- 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' |
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.
Pass sources explicitly to keep the v1 surface:
<UpupUploader sources={['local', 'url']} />driveConfigs becomes cloudDrives, and the snake_case keys become camelCase:
- 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.
<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 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.
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.
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:
- 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, replaceFilesv3 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.
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.
- 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(anUploadErrorTypevalue) toerror.code(anUpupErrorCodevalue, a string).retryableandstatusare still present. - v3 ships typed subclasses you can narrow on:
UpupAuthError,UpupNetworkError,UpupValidationError,UpupQuotaError,UpupStorageError, andUpupConfigError. UpupErrorCodeis 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.
- <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:
<UpupUploader provider="aws" mode="server" serverUrl="/api/upup" />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 = handlerServer 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.
Security upgrade
- v1's client-direct model: cloud-drive integration was client-side only —
the Google Drive
clientId/apiKeyand OneDriveclientIdwere 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
serverUrlfor OAuth and drive access, and@useupup/serverperforms the OAuth exchange and stores tokens in yourTokenStore. 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/serverissues that URL — holding the storage credentials server-side exactly as your v1tokenEndpointdid — 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.
@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.
See Server Mode — Setup for the Next.js, Express, Fastify, and Hono adapters, and Upload modes for choosing between them.
Re-check upload limits on the server
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.
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:
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.
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.
See Server Auth & Trust Model
for the full policy surface, and
POST /presign for the exact
request shape and status codes.
Full example: before and after
v1 (upup-react-file-uploader):
'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):
'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
200while checking nothing. Re-enforce the limits where the URL is signed — see Re-check upload limits on the server. sourcesdefaults 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 yourallowedFileTypesmay reject. Passsources={['local', 'url']}for the v1 surface; see Sources & cloud drives.maxFilesdefaults to 10, not 1. v1'slimitdefaulted to1. If your app relied on single-file selection, setmaxFiles={1}(or usemini, which forces a single file).onFilesUploadCompletealways received file objects, not keys — readfile.keyfor the storage key.resumable.modeis nowresumable.protocol. A leftover{ mode: 'multipart' }will not enable resumable uploads.onFileRemoveis nowonFileRemoved. 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);
miniis 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. UpupProviderandUploadAdapterno longer exist. Replace enum usages with the stringprovidervalue andsourcesids respectively.- A fresh core per mount. v3 creates and
destroy()s its engine on mount/unmount; hold state in your own app or viauseUpupUpload, not across a remount of<UpupUploader>.
Next steps
- React quickstart — the modern v3 surface end to end.
- Getting started — local collection, client uploads, and server uploads in one page.
- Server Mode — Setup — adapters, auth, and production token stores.
- Upload modes — client vs server, and when to pick each.
- Other frameworks: Vue, Svelte, Angular, Vanilla JS, Preact, Next.js.