# Use upup with AI assistants
https://useupup.com/docs/ai-assistants/
Coding agents choose a library by retrieving the docs page that answers the exact
question a developer typed, then running its quickstart. This page makes upup easy
for an assistant to discover and integrate correctly on the first try.
## Machine-readable context
upup publishes machine-readable context files that a coding agent can fetch
directly:
- **`https://useupup.com/llms.txt`** — a concise index of upup, its packages, and
the key docs pages.
- **`https://useupup.com/llms-full.txt`** — the fuller context dump
for agents that can take more input.
Point your assistant at either URL when you want it to work with upup.
## One-sentence onboarding
The fastest path: copy the "Onboard your agent to upup" prompt from
[/agent-setup/](/agent-setup/) (it is also in the docs sidebar and on the
homepage) and paste it into Claude Code, Codex, Cursor, or OpenCode. The agent
fetches `https://useupup.com/agent-setup/prompt.md`, installs the right
`@useupup/*` package, mounts the uploader, adds the presign route, and saves the
context block below into its own instructions file.
## Paste-ready context block
Drop this into your project's assistant config — `CLAUDE.md`, `.cursorrules`,
`AGENTS.md`, or similar — so your agent has an accurate, compact model of upup:
```text
# upup — MIT self-hosted file uploader (docs: https://useupup.com/docs/)
upup is one headless core plus native UI packages for six frameworks; every package renders the same uploader.
Nine published @useupup/* packages:
- @useupup/core headless engine: file state, upload pipeline (compression, HEIC, web-worker), cloud-drive plugins, i18n, theme. Zero framework deps.
- @useupup/react canonical UI (React 19). @useupup/vue, @useupup/svelte, @useupup/angular, @useupup/vanilla, @useupup/preact are native ports with the same DOM.
- @useupup/next Next.js client re-export + /server route handlers (App and Pages routers).
- @useupup/server server-mode endpoints: S3-compatible presign + multipart signing (bytes still go browser → bucket), cloud-drive token exchange + drive → bucket transfer, HMAC-signed upload-token trust model.
Client mode (default): the browser uploads straight to your storage; your app returns presigned URLs at `uploadEndpoint`. No server package required.
React example: import { UpupUploader } from '@useupup/react'; import '@useupup/react/styles'
Server mode: point the uploader at @useupup/server with mode="server" serverUrl="/api/upup".
createUpupHandler({ storage: { type: 'aws', bucket, region }, uploadTokenSecret })
— uploadTokenSecret is REQUIRED and must be >= 16 chars, or it throws at construction.
Sources: local drag-and-drop, URL/link import, camera, screen capture, and cloud drives (Google Drive, OneDrive, Dropbox, Box).
Optional: image compression, HEIC conversion, resumable uploads (tus or S3 multipart), ICU i18n, theming. Image editor is React/Preact only.
Quickstarts: https://useupup.com/docs/quickstarts//
Machine-readable docs: https://useupup.com/llms.txt (index) and https://useupup.com/llms-full.txt (full corpus). FAQ: https://useupup.com/docs/faq/
```
Every line above is verified against the packages as they ship — a snippet from
it will run rather than throw.
## Example prompt
Once the context is in place, a request like this is enough:
> Add file uploads to my SvelteKit app with upup. Use client mode with a presigned
> upload endpoint, enable the Google Drive and camera sources, and show me the
> component plus the upload-token route.
The assistant has what it needs: install `@useupup/svelte`, import the component and
its stylesheet, render ``,
and add the `sources` and `cloudDrives` config.
## Next steps
- Framework quickstarts: [React](/docs/quickstarts/react/), [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/).
- [Server Mode — Setup](/docs/guides/server-mode-setup/) for the full server walkthrough.
---
# Azure SAS Responses
https://useupup.com/docs/api-reference/azure-generate-sas-url/
Client uploads can use any endpoint that returns upup's presign contract.
```ts
type PresignedUrlResponse = {
key: string
uploadUrl: string
uploadHeaders?: Record
publicUrl?: string
downloadUrl?: string
expiresIn: number
}
```
Return headers that the browser must send with the signed request:
```ts
return Response.json({
key,
uploadUrl: sasUrl,
uploadHeaders: {
'x-ms-blob-type': 'BlockBlob',
'Content-Type': body.type || 'application/octet-stream',
},
expiresIn: 3600,
})
```
---
# Error Codes
https://useupup.com/docs/api-reference/error-codes/
Most failures upup raises are an `UpupError` (or one of its six subclasses)
carrying a `code` from the `UpupErrorCode` enum. Codes are stable strings — safe
to switch on, log, and tag in an error tracker. This page is the complete list;
[Error Handling](/docs/error-handling/) covers the narrative, and [Error
Monitoring](/docs/guides/error-monitoring/) covers wiring failures into Sentry
or your own reporter.
Everything on this page — `UpupError`, `UpupValidationError`, `UpupNetworkError`,
`UpupStorageError`, `UpupAuthError`, `UpupConfigError`, `UpupQuotaError`,
`UpupErrorCode`, and `uploadErrorFromResponse` — is exported from `@useupup/core`
and re-exported by `@useupup/react` (and therefore `@useupup/next` and
`@useupup/preact`), so an app that only installed its framework package can
import the whole surface from there without adding a direct `@useupup/core`
dependency.
The one important exception is the rejection from `upload()` itself — see
[Batch failures](/docs/api-reference/error-codes/#batch-failures-are-not-upuperrors)
before you write a handler, because it is **not** an `UpupError` and carries no
`code`.
Every class that carries a code, and the one rejection that sits outside them:
## The complete code list
`UpupErrorCode` has 24 members. `retryable` is the value the error object
carries on the `retryable` field — see [Retryability](/docs/api-reference/error-codes/#retryability)
below for what actually drives automatic retries.
| Code | Meaning | Typical trigger | Retryable |
| ------------------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `AUTH_REQUIRED` | Anonymous uploads are disabled | `@useupup/server` `/presign` or `/multipart/init` reached with no `auth`, no `getUserId`, and no `allowAnonymousUploads` — returns 403 | no |
| `AUTH_DENIED` | The upload token belongs to a different user | A multipart continuation route (sign-part / complete / abort / resume) called with a token whose `uid` is not the current user — returns 403 | no |
| `AUTH_EXPIRED` | A stored drive credential is dead or revoked | The server's drive-token refresh call is rejected by the provider; the stored tokens are deleted and the user must re-authorize. Surfaces only through the server's `onError` reporter — it is never returned as a response-body code | no |
| `AUTH_PROVIDER_ERROR` | A cloud-drive OAuth or provider call failed | OAuth code-for-token exchange returns non-2xx (server answers 502). Also the code every `UpupAuthError` carries by construction | no |
| `FILE_TOO_LARGE` | A file is over the per-file size ceiling | The file's size exceeds `maxFileSize` | no |
| `FILE_TOO_SMALL` | A file is under the per-file size floor | The file's size is below `minFileSize` | no |
| `TYPE_MISMATCH` | A file's type is not accepted | Neither the MIME type nor the extension matches `accept` | no |
| `LIMIT_EXCEEDED` | Too many files | The incoming batch would push the selection past `maxFiles` | no |
| `TOTAL_SIZE_EXCEEDED` | The selection is too large in aggregate | Existing plus incoming bytes exceed `maxTotalFileSize` | no |
| `DUPLICATE` | A content-identical file is already selected | Reserved. With `contentDeduplication` on, duplicates are dropped silently rather than raised — no code path emits this today | no |
| `MIN_FILES_NOT_MET` | Fewer files selected than required | Reserved — no code path emits this today | no |
| `UPLOAD_FAILED` | The upload could not be performed | Exactly one emitter: the resumable tus strategy is selected but the optional `tus-js-client` dependency is not installed. Despite the generic name, no other path raises it | no |
| `UPLOAD_ABORTED` | The upload was cancelled | Reserved. Aborts currently surface as an `UpupNetworkError` ("Upload aborted") carrying `NETWORK_ERROR` | no |
| `PRESIGN_FAILED` | Presigning the object URL failed | The server's `/presign` route threw — bad storage credentials, unreachable S3 endpoint, rejected key — returns 500 | no |
| `CORS_ERROR` | The bucket's CORS configuration blocked the request | Reserved for bucket-CORS diagnosis; no code path emits it today, but the i18n map already routes it to a "CORS misconfigured" message | no |
| `PIPELINE_STEP_FAILED` | A processing-pipeline step threw | Reserved — the HEIC step reports through `HEIC_CONVERSION_FAILED`, and no other step emits this today | no |
| `HEIC_CONVERSION_FAILED` | A HEIC/HEIF image could not be decoded | The optional `libheif-js` dependency is not installed, or the file contains no decodable image | no |
| `NETWORK_ERROR` | The transport itself failed | An XHR `error` or `abort` event during a direct PUT, or any `UpupNetworkError`. Note that the error thrown once retries are exhausted is the last _real_ failure, which is often an `UpupStorageError`, not this code | **yes** |
| `TIMEOUT` | An operation ran past its deadline | Reserved — no code path emits this today | no |
| `STORAGE_ERROR` | An S3 / S3-compatible storage operation failed | A direct PUT returning non-2xx, any multipart init / sign-part / complete / abort / resume failure, a failed drive list or transfer, or the server router's last-resort 500. Also reported (not returned) when a post-completion hook throws after a durably-completed upload the client already got a 200 for, and when the health probe finds storage unreachable | class-dependent |
| `QUOTA_EXCEEDED` | A configured storage quota is exhausted | The code every `UpupQuotaError` carries by construction | no |
| `NO_UPLOAD_TARGET` | No upload destination is configured | The default code for `UpupConfigError` — no `uploadEndpoint` / provider wiring to upload to | no |
| `BAD_REQUEST` | The request or API call is malformed | Server-side: an unparseable JSON body or invalid file metadata (400). Client-side: calling `addFiles` / `setFiles` / `upload` / `resume` / `retry` after `destroy()`, or passing an unknown file ID to `replaceFile` / `reorderFiles` | no |
| `NOT_FOUND` | The addressed resource is gone server-side | `@useupup/server` `/multipart/resume` asked to re-attach to a multipart upload the storage provider no longer has — completed, aborted, or reaped by an `AbortIncompleteMultipartUpload` lifecycle rule — returns 404. Deliberately a 4xx: the client drops its saved session and starts a fresh upload rather than retrying something that cannot return | no |
Codes marked _reserved_ are part of the published enum and safe to reference in
your own `switch` statements — they simply have no emitter in upup today.
### Retryability
`retryable` is a per-class constant, not a per-code lookup: `UpupNetworkError`
sets it to `true` and every other class sets it to `false`.
Crucially, **the code and the flag come from different places**, so you cannot
infer one from the other. `uploadErrorFromResponse` picks the class from its
`kind` argument — which fixes `retryable` — and then overwrites `code` with
whatever machine code the response body supplied. A code can therefore ride on a
class it does not "belong" to. The server-side drive transfer is a real example:
it builds its error with `kind: 'network'` (so `retryable` is `true`), and the
server answers a failed drive request with `STORAGE_ERROR` in the body, so the
client genuinely sees `STORAGE_ERROR` with `retryable: true`.
Never branch on `code` to decide whether something is retryable, or vice versa.
Read `retryable` for the flag and `code` for the cause.
Automatic retries do **not** consult that flag. The upload manager retries every
failed attempt up to `maxRetries` times except three cases, which fail
immediately:
- the upload was aborted,
- the failure is an `UpupNetworkError` with a 4xx status (a client error — a
retry would fail identically),
- the failure is an `UpupNetworkError` with status `0`, which is how a rejecting
`isSuccessfulCall` is reported.
Treat `retryable` as a hint for your own UI and reporting logic, and
[`maxRetries`](/docs/api-reference/upupuploader/optional-props/#maxretries) as
the control for upup's own retry loop.
## The error classes
Seven classes, all exported from `@useupup/core`. Each subclass fixes the `code`
it carries (except `UpupConfigError`, whose code is overridable) and adds the
fields you need to act on the failure.
| Class | Code it carries | Thrown when | Extra fields |
| --------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `UpupError` | whatever the caller passes | Base class. Raised directly for lifecycle and API misuse (`BAD_REQUEST`), HEIC decode failures, and a missing optional upload dependency | `code`, `retryable`, `status?` |
| `UpupAuthError` | `AUTH_PROVIDER_ERROR` | A cloud-drive OAuth or provider call fails; also what `uploadErrorFromResponse` builds for `kind: 'auth'` | `provider` |
| `UpupNetworkError` | `NETWORK_ERROR` | Transport failure or abort — the only class that is `retryable` | `status?` |
| `UpupValidationError` | the restriction reason itself | A file fails a size, type, or count check | `reason`, `file` |
| `UpupQuotaError` | `QUOTA_EXCEEDED` | A configured quota is exceeded | `limit`, `used` |
| `UpupStorageError` | `STORAGE_ERROR` | An S3 / storage operation fails; also what `uploadErrorFromResponse` builds for `kind: 'storage'` | `provider`, `operation` |
| `UpupConfigError` | `NO_UPLOAD_TARGET` by default, overridable via the constructor | Configuration is missing or invalid | — |
`UpupStorageError.operation` is one of `'presign'`, `'upload'`,
`'multipart-init'`, `'multipart-complete'`, `'multipart-sign-part'`,
`'multipart-abort'`, or `'multipart-resume'` — enough to tell a failed presign
apart from a failed part upload without parsing messages.
### Batch failures are not `UpupError`s
When one or more files in a run fail, `upload()` rejects with an
`UpupUploadBatchError`. It extends plain `Error`, **not** `UpupError` — it has no
`code` and no `retryable`. This is the single most common rejection you will
handle, so a handler that only checks `instanceof UpupError` will fall through to
its "unknown error" branch on the ordinary failure path.
The batch error is a wrapper, and the coded per-file errors are inside it:
```typescript
readonly errors: { file: UploadFile; error: Error }[]
```
Each `error` in that array is the real, coded failure for that file — an
`UpupStorageError`, `UpupNetworkError`, and so on. The same per-file errors also
arrive individually on the `upload-error` event as they happen, which is usually
the better place to react; the batch rejection is best treated as "the run did
not fully succeed" rather than as a diagnosis.
One caveat when narrowing: `UpupUploadBatchError` is **not exported** from
`@useupup/core` or `@useupup/core/internal`, so you cannot `instanceof` it. Detect
it by name or by shape:
```typescript
function isBatchError(
error: unknown,
): error is Error & { errors: { file: File; error: Error }[] } {
return error instanceof Error && error.name === 'UpupUploadBatchError'
}
```
### Narrowing by class, then by code
`instanceof` gets you the extra fields; the `code` gets you the specific cause.
Reach for the class first, because that is what makes `error.file` or
`error.operation` type-safe:
```typescript
import {
UpupError,
UpupErrorCode,
UpupNetworkError,
UpupStorageError,
UpupValidationError,
} from '@useupup/core'
function report(error: unknown): string {
// Check this FIRST — the rejection from upload() is a batch wrapper that is
// not an UpupError, so every instanceof below would miss it.
if (isBatchError(error)) {
return error.errors.map(entry => report(entry.error)).join('; ')
}
if (error instanceof UpupValidationError) {
// `reason` is narrowed to RestrictionFailedReason
return `${error.file.name} rejected (${error.reason})`
}
if (error instanceof UpupStorageError) {
return `S3 ${error.operation} failed on ${error.provider}`
}
if (error instanceof UpupNetworkError) {
return error.status === undefined
? 'Connection dropped.'
: `Transport failed with HTTP ${error.status}.`
}
if (error instanceof UpupError) {
switch (error.code) {
case UpupErrorCode.PRESIGN_FAILED:
return 'Your token endpoint returned an error.'
case UpupErrorCode.AUTH_REQUIRED:
case UpupErrorCode.AUTH_DENIED:
return 'Sign in again to upload.'
case UpupErrorCode.HEIC_CONVERSION_FAILED:
return 'This HEIC image could not be converted.'
default:
return error.retryable
? 'Transient failure — safe to retry.'
: 'Permanent failure — needs attention.'
}
}
return 'Unknown error.'
}
```
## Restriction reasons
`UpupValidationError.reason` is typed as `RestrictionFailedReason` — the subset
of codes a file-level check can produce, plus one string that is not part of the
code enum:
```typescript
type RestrictionFailedReason =
| UpupErrorCode.TYPE_MISMATCH
| UpupErrorCode.FILE_TOO_LARGE
| UpupErrorCode.FILE_TOO_SMALL
| UpupErrorCode.LIMIT_EXCEEDED
| UpupErrorCode.TOTAL_SIZE_EXCEEDED
| UpupErrorCode.DUPLICATE
| 'BEFORE_FILE_ADDED_REJECTED'
```
The per-file checks run in a fixed order — type, then maximum size, then minimum
size — so the first violation reported for a file is the most specific one.
`LIMIT_EXCEEDED` and `TOTAL_SIZE_EXCEEDED` are batch-level instead: they are
raised once for the whole incoming batch, with `file` set to the first file in
it rather than to a single offender.
`'BEFORE_FILE_ADDED_REJECTED'` is the reason reserved for a host application
vetoing a file before it enters the selection. It is a bare string, not a
`UpupErrorCode` member, so compare it literally; like `DUPLICATE`, no code path
emits it today.
## Errors from your own endpoints
When you host the presign or token endpoint yourself, `uploadErrorFromResponse`
is what turns a failed HTTP response into a typed, code-carrying error. Four of
the five upload strategies route their failures through it — direct PUT,
multipart, server credentials, and server-side drive transfer — so a machine code
your endpoint returns arrives intact on `error.code`.
The exception is the tus strategy, which rejects with whatever error
`tus-js-client` produced, unwrapped. Those errors are not `UpupError`s and carry
no `code`, so a handler that reads `error.code` will come up empty on tus
uploads.
The presign call a custom `uploadEndpoint` makes routes through it too, so the
sentence your endpoint writes into a non-2xx body is what `onError` receives —
you do not have to recover it from an HTTP status. When the body is empty,
unreadable, or an HTML error page a reverse proxy wrote with no error code of
its own, the message stays `Presign request failed: `, the
wording that path has always thrown.
```typescript
import { uploadErrorFromResponse } from '@useupup/core'
const err = uploadErrorFromResponse({
status: response.status,
statusText: response.statusText,
body: await response.text(),
kind: 'storage', // 'storage' | 'auth' | 'network'
operation: 'presign', // required when kind is 'storage'
provider: 'S3', // defaults to 'server'
})
```
`kind` picks the class: `'auth'` builds an `UpupAuthError`, `'storage'` an
`UpupStorageError`, and anything else an `UpupNetworkError`. The originating
HTTP status is always copied onto `error.status`.
### How the body is parsed
`parseErrorBody` is the exported helper behind it, and it tries three shapes in
order:
1. **JSON** — a `code` field and an `error` (or `message`) field. This is the
convention `@useupup/server` emits, so a server-mode failure reaches the
client with the same machine code the server logged.
2. **S3-style XML** — `` and `` inside an `` envelope.
This is how S3 and S3-compatible providers report a rejected direct PUT, so
provider codes like `SignatureDoesNotMatch` or `EntityTooLarge` survive.
3. **Plain text** — the first 200 characters of the body become the message,
with no code.
If the body yields no code, `error.code` falls back to the class default and the
message falls back to `""`. To make your own endpoint's
failures legible on the client, return JSON with a `code` field:
```typescript
return Response.json(
{ error: 'Bucket is read-only', code: 'STORAGE_ERROR' },
{ status: 500 },
)
```
Anything you put in `code` is preserved verbatim — it does not have to be an
`UpupErrorCode` member.
## Turning codes into user-facing text
`errorCodeToMessageKey(code)` in `packages/core/src/i18n/error-code-map.ts` maps
a machine code to a key in the localized `ErrorMessages` catalog, and it is the
one place that mapping lives. Note that it returns a **message key**, not a code
— the two namespaces look similar and are easy to conflate.
Coverage is partial. Eight of the 24 `UpupErrorCode` members have a dedicated
mapping: `PRESIGN_FAILED`, `STORAGE_ERROR`, `BAD_REQUEST`, `AUTH_PROVIDER_ERROR`,
`AUTH_EXPIRED`, `AUTH_DENIED`, `CORS_ERROR`, and `QUOTA_EXCEEDED`. The map also
handles values that are not enum members at all: `@useupup/server`'s upload-token
codes (`expired`, `bad_signature`, `malformed`), S3 provider codes
(`AccessDenied`, `SignatureDoesNotMatch`, `EntityTooLarge`, `NoSuchBucket`,
`InvalidBucketName`), and the drive controller's `UNAUTHENTICATED`.
Everything else — the other 16 enum members included — falls through to the
`uploadFailedWithCode` message, and **that message interpolates the raw code
together with your own error sentence**
(`Upload failed with error code {code}: {message}`). So an unmapped code _is_
shown to the end user, and so is whatever `error`/`message` your endpoint
returned. Only a completely absent code is code-free, falling back to the plain
`uploadFailed` message. Both slots are optional for a translator: override
`uploadFailedWithCode` for your locale to reorder them, drop the code, or show
only your own wording. The interpolated value is always rendered as text, never
as markup.
See [Localization](/docs/localization/) for overriding those strings per locale.
---
# File Upload Events
https://useupup.com/docs/api-reference/events/
Every upload, file, drag, pipeline, and UI interaction in upup flows through one
typed event bus on `UpupCore`. Each framework's callback props are a convenience
layer over that bus, and a lossy one — the bus carries events no prop exposes,
and a few props fire on paths that emit no event at all. When in doubt, listen
to the event.
## Listening
With the visual uploader, use the callback props:
```tsx
console.log('started')}
onFileUploadProgress={(file, p) => console.log(file.name, p.percentage)}
onUploadComplete={files => console.log(files.map(f => f.key))}
onError={message => console.error(message)}
/>
```
Headless, subscribe to the bus directly. `on()` returns an unsubscribe
function:
```ts
import { UpupCore } from '@useupup/core'
const core = new UpupCore({ uploadEndpoint: '/api/upload-token' })
const off = core.on('upload-progress', ({ fileId, loaded, total }) => {
console.log(fileId, Math.round((loaded / total) * 100))
})
core.on('upload-error', ({ error, file }) => {
console.error(file?.name ?? 'run', error.message)
})
// Later:
off()
core.destroy()
```
The React hook `useUpupUpload` exposes the same `on(event, handler)`, and the
uploader ref exposes the underlying `core`. See
[Headless Usage](/docs/guides/headless/) for both.
Bare event names are the typed catalog below and nothing else — an unknown bare
name is a compile error at the `on` / `emit` site.
> **There is exactly one upload-failure event: `upload-error`.** A bare
> `'error'` event does not exist. Failed `resume()` calls route through
> `upload-error`, and so does every pipeline step failure except HEIC
> conversion, which downgrades to a `pipeline-error` diagnostic.
> Cloud-drive plugins emit **namespaced** `:` names
> (e.g. `google-drive:files-loaded`, `one-drive:session-expired`) which pass
> through the bus untyped and are not part of this catalog — see
> [Credentials & Cloud Drives](/docs/credentials-configuration/).
Events whose payload is listed as `{}` carry an empty object; the payload
argument exists but has no fields.
## Upload lifecycle
A run drives one status projection, and the events below are emitted as it moves
between states:
| Event | Payload | Fires when |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state-change` | `{ status?: UploadStatus; error?: Error; files?: Map; progress?: { totalFiles: number; completedFiles: number; percentage: number } }` | Any observable state moves — the catch-all every framework binding re-renders from. Emitted alongside most other events, carrying only the slice that changed. |
| `upload-start` | `{ retry?: boolean; fileId?: string }` | A run begins. `retry: true` (with the optional `fileId`) marks a retry run rather than a fresh one. |
| `file-upload-start` | `{ file: UploadFile }` | An individual file starts transferring. |
| `upload-progress` | `{ fileId: string; loaded: number; total: number }` | Byte progress advances for one file. |
| `upload-success` | `{ file: UploadFile; result: UploadResult }` | One file finishes successfully; `result.key` is its storage key. |
| `upload-error` | `{ error: Error; file?: UploadFile }` | A file fails (payload carries `file`) **or** the run ends terminally (no `file`). The only failure event — but see the double-emit warning below. |
| `upload-all-complete` | `UploadFile[]` | Every file in the run has a storage key — the batch is done. |
| `upload-pause` | `{}` | `core.pause()` pauses an in-flight run. |
| `upload-resume` | `{}` | `core.resume()` restarts a paused run. |
| `upload-cancel` | `{}` | `core.cancel()` aborts the run; pending files return to `IDLE`. |
| `retry` | `{ fileId?: string }` | `core.retry(fileId?)` is called, before the retry run starts. Omitting `fileId` retries every unfinished or failed file. |
| `destroyed` | `{}` | `core.destroy()` runs. Terminal — afterwards `upload`/`resume`/`retry`/`addFiles`/`setFiles` throw. |
> **One failed file emits `upload-error` twice.** The per-file emission fires
> first (with `file`), then the batch rejects and the terminal handler emits a
> second time (without `file`, carrying an aggregate
> `UpupUploadBatchError`). Anything bound to `upload-error` — including the
> `onError` prop — therefore runs **twice** for a single failure. Deduplicate on
> `file` being present if you only want the per-file signal, and treat the
> `file`-less emission as "the run is over".
> **Vanilla only:** `@useupup/vanilla` also emits `upload-error` when a link
> import fails, so a URL-fetch failure reaches the same channel as an upload
> failure. React's URL path calls `onError` without emitting the event. Don't
> assume `upload-error` implies a transfer was attempted.
## File operations
| Event | Payload | Fires when |
| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `files-added` | `UploadFile[]` | One or more files pass validation and enter core state (only fires when at least one was accepted). |
| `file-removed` | `UploadFile` | A single file is removed via `removeFile(id)`. |
| `file-rejected` | `{ count: number }` | Some of an `addFiles` batch were filtered out — `count` is how many. The accepted remainder still lands. |
| `file-replaced` | `{ file: UploadFile }` | A file's content is swapped in place, keeping its id (the image editor's save path). |
| `files-cleared` | `{}` | `removeAll()` empties the list and clears any crash-recovery snapshot. |
| `files-set` | `{ count: number }` | `setFiles()` replaces the whole selection; `count` is the resulting size. |
| `files-reordered` | `{ fileIds: string[] }` | `reorderFiles()` changes the display order; `fileIds` is the new order. |
| `restriction-failed` | `{ error: unknown }` | `addFiles()` throws instead of filtering — a hard validation failure (type, size, count). The error is rethrown to the caller. Nothing in upup subscribes to this event; the built-in UI reacts by catching the rethrow instead. |
## Plugins and options
| Event | Payload | Fires when |
| ------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------- |
| `plugin-registered` | `{ name: string }` | `core.use(plugin)` registers a plugin, after its `init(emitter)` hook runs. |
| `options-updated` | `{ partial: Partial }` | `updateOptions()` merges new options — how framework bindings forward changed props to core. |
## Recovery
| Event | Payload | Fires when |
| ------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `snapshot-restored` | `{ count: number; status: UploadStatus }` | `core.restore(snapshot)` rehydrates files and status from a snapshot. |
| `crash-recovery-restored` | `{}` | A persisted IndexedDB session is recovered after a refresh or crash. Files that were mid-flight come back as `PAUSED`. Emitted after the `snapshot-restored` it triggers. |
## UI flow
| Event | Payload | Fires when |
| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------ |
| `done` | `{}` | The user dismisses a finished run (the Done action), after `onDoneClicked` and before the reset. |
| `state-reset` | `{}` | The uploader returns to its empty initial state. |
| `auto-upload` | `{ count: number }` | `autoUpload` is on and newly added files trigger an upload automatically; `count` is how many. |
| `connection-online` | `{}` | The browser regains network connectivity. |
| `connection-offline` | `{}` | The browser loses network connectivity. |
## Image editor
React and Preact only — the other frameworks intentionally stub the editor.
| Event | Payload | Fires when |
| --------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------- |
| `image-editor-open` | `{ file: UploadFile }` | The editor opens for a file, whether by user action or `imageEditor.autoOpen`. |
| `image-editor-cancel` | `{ file: UploadFile }` | The editor closes without saving. If more files are queued, the next one opens. |
| `image-editor-save` | `{ file: UploadFile; original: UploadFile }` | An edit is saved. `file` is the new content, `original` is what it replaced (same id). |
## Drag, drop, and paste
| Event | Payload | Fires when |
| --------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `drag-over` | `{}` | A drag hovers the dropzone. Bound straight to the DOM `dragover` event with no enter-transition guard, so it fires **repeatedly** throughout the hover — debounce it, don't treat it as an enter signal. |
| `drag-leave` | `{}` | A drag leaves the dropzone without dropping. Leaves into a child element are filtered out, so this fires once per real exit. |
| `drop` | `{ files: File[] }` | Files are dropped and accepted. Not emitted when a folder drop is blocked and nothing else came with it. |
| `folder-drop-blocked` | `{ acceptedFiles: number }` | A dropped folder was ignored because `folderUpload.allowDrop` is off. `acceptedFiles` counts the loose files that still went through. |
| `paste` | `{ files: File[] }` | Files are pasted with `enablePaste` on. A pasted file is renamed to `pasted-.` only when the clipboard gave it no name or the literal name `image.png`; any other name is kept as-is. |
## Pipeline diagnostics
The processing pipeline (hash, HEIC, EXIF, thumbnail, compress) reports through
these as it works through each file.
| Event | Payload | Fires when |
| ------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pipeline-start` | `{ fileId: string; steps: string[] }` | Processing begins for a file. `steps` lists every **configured** step — a step whose `shouldProcess` returns false is still listed here but never runs, so it emits no `pipeline-step`. |
| `pipeline-step` | `{ fileId: string; step: string }` | A step **finished**. It is emitted after the step resolves, not before it starts, so a step that never completes produces no event. |
| `pipeline-complete` | `{ fileId: string }` | All steps finished for that file. |
| `pipeline-error` | `{ scope: string; name: string; message: string }` | HEIC conversion failed. This is the only source — the pipeline engine has no error handling of its own. |
> **`pipeline-error` is not the general step-failure event.** Only the HEIC step
> catches its own errors and reports them here, downgrading a failed conversion
> to a diagnostic while the run continues. Every **other** step failure
> propagates out of the pipeline uncaught and fails the whole run through
> `upload-error`. So the absence of `pipeline-error` says nothing about whether
> processing succeeded.
## UI telemetry
Emitted by every framework's UI layer with identical payloads (React is the
payload canon). Useful for analytics on how people drive the picker.
| Event | Payload | Fires when |
| -------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source-click` | `{ sourceId: FileSource }` | A source tile is clicked in the source selector. |
| `source-view-cancel` | `{ sourceId: FileSource \| undefined }` | The user backs out of an open source view. |
| `browse-files` | `{}` | The native file picker is opened for a normal file browse. |
| `folder-select` | `{ count: number }` | A folder is chosen. `count` is the file count via the File System Access API, or `0` on the `webkitdirectory` input fallback where the count isn't known yet. |
| `url-submit` | `{ url: string }` | The link-import form is submitted. |
| `url-fetch` | `{ file: File }` | A link import finishes downloading and produced a file. |
| `url-fetch-cancel` | `{ url: string }` | An in-flight link import is aborted. |
| `camera-capture` | `{ dataUrl: string }` | A camera photo is captured, before the user confirms it. |
| `camera-confirm` | `{ file: File }` | The captured photo is confirmed and added. |
| `file-preview-open` | `{ fileId: string; fileName: string }` | A file preview overlay opens. |
| `file-preview-close` | `{ fileId: string; fileName: string }` | A file preview overlay closes. |
## React callback props
`` accepts these handlers. They come from the shared
`UploaderBaseProps` type, so React, Vue, Svelte, Angular, and Preact all take
the same set (each with its own binding syntax). Most are driven by a core
event — subscribe to the event instead when you need the full payload, ordering
guarantees, or a listener outside the component.
> **Vanilla is a different surface.** `@useupup/vanilla`'s `createUploader`
> takes `CreateUploaderOptions`, not `UploaderBaseProps`, and accepts only
> eleven callbacks: `onFileAdded`, `onFileRemoved`, `onUploadProgress`,
> `onUploadComplete`, `onDoneClicked`, `onIntegrationClick`, `onFilesDragOver`,
> `onFilesDragLeave`, `onFilesDrop`, `onWarn`, `onError`. Note `onFileAdded`
> rather than `onFilesSelected`, and a single `onUploadProgress` rather than the
> per-file/aggregate pair. `onStatusChange`, `onPrepareFiles`,
> `onRestrictionFailed`, `onFileTypeMismatch`, and the per-file upload callbacks
> have no vanilla equivalent — subscribe to the events directly there.
| Prop | Signature | Driven by |
| ----------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `onFilesSelected` | `(files: UploadFile[]) => void` | `files-added` |
| `onDoneClicked` | `() => void` | `done` (called just before the event) |
| `onPrepareFiles` | `(files: UploadFile[]) => Promise` | No event — an async gate awaited at the start of `startUpload()`; return the list to upload |
| `onFileClick` | `(file: UploadFile) => void` | No event — a click on a file row in the list |
| `onIntegrationClick` | `(integrationType: string) => void` | `source-click` (called just before the event) |
| `onUploadStart` | `() => void` | `upload-start` |
| `onFileUploadStart` | `(file: UploadFile) => void` | `file-upload-start` |
| `onFileUploadProgress` | `(file: UploadFile, progress: { loaded: number; total: number; percentage: number }) => void` | `upload-progress` (per file; `percentage` is computed for you) |
| `onFilesUploadProgress` | `(completedFiles: number, totalFiles: number) => void` | `upload-progress` (batch aggregate) |
| `onFileUploadComplete` | `(file: UploadFile, key: string) => void` | `upload-success` |
| `onFilesUploadComplete` | `(files: UploadFile[]) => void` | `upload-all-complete` |
| `onUploadComplete` | `(files: UploadFile[]) => void` | `upload-all-complete` (same event as above; both fire) |
| `onStatusChange` | `(status: string) => void` | `state-change` — the projected status, lowercased and deduplicated so it fires once per real transition |
| `onFileRemoved` | `(file: UploadFile) => void` | `file-removed` |
| `onFilesDragOver` | `(files: File[]) => void` | `drag-over` — fires repeatedly during the hover, and `files` is effectively always `[]` (see below) |
| `onFilesDragLeave` | `(files: File[]) => void` | `drag-leave` — `files` is effectively always `[]` (see below) |
| `onFilesDrop` | `(files: File[]) => void` | `drop` (called just before the event) |
| `onFileTypeMismatch` | `(file: File, acceptedTypes: string) => void` | No event — the built-in UI's rejection path only (see below) |
| `onRestrictionFailed` | `(file: File, reason: 'TYPE_MISMATCH' \| 'FILE_TOO_LARGE' \| 'FILE_TOO_SMALL' \| 'LIMIT_EXCEEDED') => void` | No event — the built-in UI's rejection path only (see below) |
| `onBeforeFileAdded` | `(file: File) => boolean \| File \| undefined \| Promise` | No event — an async filter run per file before it is added. Return `false` to reject, a `File` to substitute, `true`/`undefined` to accept |
| `onError` | `(errorMessage: string) => void` | `upload-error`, plus validation failures from `addFiles()`; delivers a message string |
| `onWarn` | `(warningMessage: string) => void` | No event of its own — non-fatal notices such as an ignored folder drop (which also emits `folder-drop-blocked`) |
| `onFileProcessed` | `(file: UploadFile, data: Record) => void` | No core event — the server's SSE message on `processingEndpoint` (opened after the whole batch, see below) |
`onFilesUploadComplete` and `onUploadComplete` are both invoked from
`upload-all-complete` — pick one.
### Caveats worth knowing
**The rejection callbacks are UI-path only, and the reason is guessed from text.**
`onFileTypeMismatch` and `onRestrictionFailed` are _not_ wired to
`restriction-failed`. The built-in UI wraps its `addFiles` call in a try/catch
and classifies the thrown error by substring-matching its **message** —
`'type'`, `'limit'`, `'below'`, `'size'` — to pick the reason code. Two
consequences: calling `core.addFiles()` yourself emits `restriction-failed` but
invokes neither callback, and the reason code is a heuristic over
human-readable text rather than a structured field. For robust handling,
subscribe to `restriction-failed` and inspect the error yourself. Both callbacks
fire for a type mismatch; `onRestrictionFailed` alone covers the size and count
cases.
**Drag callbacks never receive files.** `onFilesDragOver` and `onFilesDragLeave`
are handed `e.dataTransfer.files`, which browsers keep empty during a drag
(the DataTransfer is in protected mode until the drop completes). Expect `[]`
and use these purely as hover signals — real files arrive only in `onFilesDrop`.
**SSE processing is per batch, not per file.** Despite its name and per-file
signature, `onFileProcessed` is driven by a connection opened once the _entire_
batch finishes: the handler folds into `onFilesUploadComplete` and then opens one
`EventSource` per completed file. No connection opens after each individual
upload, so with a long batch the first file's server-side processing isn't
watched until the last file lands.
## Next steps
- [Event Handlers](/docs/api-reference/upupuploader/event-handlers/) — the short
version of the callback props.
- [Headless Usage](/docs/guides/headless/) — drive `UpupCore` and its bus with no
upup UI at all.
- [Error Handling](/docs/error-handling/) — the `UpupError` taxonomy behind
`upload-error`.
---
# S3 Presign Responses
https://useupup.com/docs/api-reference/s3-generate-presigned-url/
For client uploads, return a presigned object URL and any headers that were part
of the signature.
```ts
type PresignedUrlResponse = {
key: string
uploadUrl: string
uploadHeaders?: Record
publicUrl?: string
downloadUrl?: string
expiresIn: number
}
```
Do not return browser-forbidden headers such as `Content-Length` in
`uploadHeaders`: the browser sets them itself and ignores a script-set value.
Signing `Content-Length` is fine as long as it equals the file size, which is
how `@useupup/server` limits an upload to the size it approved. If the content
type is signed, return it in `uploadHeaders` so upup can send the exact same
value. [Upload Files to S3 from React with Presigned URLs](/docs/guides/s3-presigned-url-upload-react/)
builds a complete endpoint.
```ts
return Response.json({
key,
uploadUrl,
uploadHeaders: {
'Content-Type': body.type || 'application/octet-stream',
},
downloadUrl,
expiresIn: 3600,
})
```
Use `@useupup/server` when you want the server package to host these routes.
---
# Server HTTP API
https://useupup.com/docs/api-reference/server-http/
`createUpupHandler(config)` returns one `(req: Request) => Promise`
function that serves every route below. This page is the wire-level contract:
what each route accepts, what it returns, and which status codes it can produce.
Reach for it when you are writing a non-upup client, proxying the handler, or
reading a failing request in your logs. For wiring the handler into a framework,
see [Server Mode — Setup](/docs/guides/server-mode-setup/); for why the 403s
exist, see [Server Auth & Trust Model](/docs/guides/server-auth/).
## Routing model
Routes are matched on the **path suffix**, so the handler works at whatever
prefix you mount it (`/api/upup`, `/upload`, the root). Every example below is
written relative to that mount point.
Trailing slashes are tolerated: the handler strips them before matching, so a
framework that normalizes URLs (Next.js `trailingSlash: true` 308s a
`POST /presign` to `/presign/` with method and body intact) still hits the
route instead of falling through to 404.
Two routes run **before** the global `auth` gate — `OPTIONS` preflight and
`GET /health` — so browsers can preflight and uptime probes can poll without
credentials. Everything else passes through `config.auth` first, when you have
configured it.
## Route table
| Method | Path | Auth | Success |
| --------- | --------------------------- | ----------------------------------------------- | -------------------- |
| `OPTIONS` | any path | none (before the gate) | `204`, no body |
| `GET` | `/health` | none (before the gate) | `200` always |
| `POST` | `/presign` | `auth` / `getUserId` / `allowAnonymousUploads` | `200` presigned PUT |
| `POST` | `/multipart/init` | `auth` / `getUserId` / `allowAnonymousUploads` | `200` + upload token |
| `POST` | `/multipart/sign-part` | upload token (owner-bound when `getUserId` set) | `200` presigned part |
| `POST` | `/multipart/complete` | upload token (owner-bound when `getUserId` set) | `200` final object |
| `POST` | `/multipart/abort` | upload token (owner-bound when `getUserId` set) | `200` `{ ok: true }` |
| `POST` | `/multipart/resume` | upload token (owner-bound; expiry relaxed) | `200` parts + token |
| `GET` | `/auth/:provider` | resolved user id | `302` to provider |
| `GET` | `/auth/:provider/cb` | OAuth `state` (single-use, 10 min) | `200` HTML |
| `GET` | `/files/:provider` | resolved user id + stored drive tokens | `200` file list |
| `POST` | `/files/:provider/transfer` | resolved user id + stored drive tokens | `200` stored object |
`:provider` is one of `google-drive`, `one-drive`, `dropbox`, `box` — the
kebab-case wire form. Anything else returns `400`.
Any method/path pair not in the table falls through to
`404 {"error":"Not found"}` — including a `GET /presign` or a
`POST /files/:provider` without the `/transfer` suffix.
`POST /multipart/resume` is the one route you can switch off: setting
`multipartResumeWindowSeconds: 0` stops it from being routed at all, so it 404s
like any unknown path. That is deliberate — a server with resume disabled is
indistinguishable from one too old to have the route, and upup clients treat
both the same way.
## Response conventions
Every response the handler produces — success, error, redirect, preflight —
carries an **`x-upup-request-id`** header holding a fresh UUID for that request.
The same id appears in the `onError` log record, so a client-side failure
correlates to exactly one server log line. There is no route that skips it.
CORS headers are added to every response too, when `config.cors` is set and the
request's `Origin` matches `allowedOrigins` (or the list contains `*`). The
matched origin is reflected rather than a literal `*`; a bare `*` goes out only
to origin-less, non-browser callers.
`Access-Control-Allow-Credentials: true` is sent **only** on a concrete origin
match — a wildcard-only allowlist gets public, non-credentialed CORS.
Failure bodies are uniform:
```json
{ "error": "human-readable message", "code": "MACHINE_CODE" }
```
The `code` field is present on coded failures (the `UpupErrorCode` values plus
the upload-token codes); a few plain validation rejections carry `error` alone.
The real cause — stack, provider response — goes to your
[`onError` seam](/docs/guides/error-monitoring/), never to the client.
An unhandled throw anywhere in routing is caught by a single transport safety
net that returns a logged, CORS-headered
`500 {"error":"Internal error","code":"STORAGE_ERROR"}`. Adapters do not need a
catch of their own.
## `OPTIONS` — preflight
Returns `204` with no body, the CORS headers, and `x-upup-request-id`. Matched
before the auth gate and before any path matching, so it answers for every path
the handler is mounted under.
## `GET /health`
Answers "is this instance configured, and can it reach storage?" in one
unauthenticated request, without performing an upload. It **always returns
`200`** — a liveness probe should not restart a container because S3 blipped —
so read the `status` field, not the HTTP code.
```json
{
"status": "ok",
"checks": { "config": "ok", "storage": "ok" },
"summary": {
"storageType": "aws",
"anonymousUploads": false,
"anonymousDrives": false,
"driveProviders": 2,
"uploadTokenTtlSeconds": 3600
}
}
```
| Field | Values |
| ------------------------------- | ------------------------------------------------------------------------ |
| `status` | `ok` when both checks pass, otherwise `degraded` |
| `checks.config` | `ok` / `incomplete` — bucket, region, and a ≥ 16-char secret are all set |
| `checks.storage` | `ok` / `error` — a `HeadBucket` probe, cached for 30 s |
| `summary.storageType` | the configured `storage.type` label |
| `summary.anonymousUploads` | whether `allowAnonymousUploads` is on |
| `summary.anonymousDrives` | whether `allowAnonymous` (drive/tokenStore scoping) is on |
| `summary.driveProviders` | count of configured OAuth providers |
| `summary.uploadTokenTtlSeconds` | lifetime of an issued multipart upload token |
No secret **value** is ever returned. Setting
`health: { exposeSecretFingerprint: true }` adds one extra field:
```json
{ "uploadTokenFingerprint": "9f2c41ab" }
```
That is the first 8 hex characters of `SHA-256(uploadTokenSecret)` — enough to
confirm every instance in a fleet shares the same secret (a rolling redeploy
that half-rotated it shows two different fingerprints), and not reversible to
the secret itself. It is off by default.
## `POST /presign`
Issues a presigned `PUT` so the browser uploads bytes straight to storage. The
server chooses the object key; the client never proposes one.
**Auth.** This is a capability-granting route, so it is closed by default: with
none of `auth`, `getUserId`, or `allowAnonymousUploads` configured it returns
`403` `AUTH_REQUIRED`. That is the intended posture — configure one of the three
rather than working around it.
**Request**
```ts
type FileMetadata = {
name: string // non-empty
type: string // MIME type; may be empty, but then it must pass allowedTypes
size: number // bytes, finite, >= 0
metadata?: Record // free-form; see below
}
```
`metadata` is optional, opaque routing input: the handler neither reads nor
validates it, and passes it through to `keyStrategy` and a `storage` resolver.
It is client-controlled — a server that routes on it must treat it like a query
parameter. `/multipart/init` accepts the same field.
**Response `200`** — the `PresignedUrlResponse` shape from `@useupup/core`:
```ts
type PresignedUrlResponse = {
key: string
publicUrl?: string
downloadUrl?: string
uploadUrl: string
uploadHeaders?: Record
expiresIn: number
}
```
This handler returns `key`, `uploadUrl`, `downloadUrl`, `uploadHeaders`, and
`expiresIn: 3600`. The PUT signature covers both `content-type` and
`content-length`, so a body larger than the approved size fails at S3 rather
than silently landing. `downloadUrl` is a separately signed `GET`, valid for
three days unless `downloadUrlExpiresIn` says otherwise.
**Status codes**
| Status | Body | Cause |
| ------ | ------------------------------------------------------------- | --------------------------------------------- |
| `400` | `{ "error": "Invalid JSON body", "code": "BAD_REQUEST" }` | unparseable body |
| `400` | `{ "error": "Invalid file metadata", "code": "BAD_REQUEST" }` | missing/wrong-typed `name`, `type`, or `size` |
| `401` | `{ "error": "Unauthorized" }` | your `auth` gate returned `false` |
| `401` | `{ "error": "Unauthenticated" }` | `getUserId` returned `null` |
| `403` | `{ "error": "…", "code": "AUTH_REQUIRED" }` | no auth path configured |
| `403` | `{ "error": "Upload rejected" }` | your `hooks.onBeforeUpload` returned `false` |
| `403` | `{ "error": "…", "code": "…" }` | your `hooks.onBeforeUpload` threw an `UpupError` — its own message and code |
| `413` | `{ "error": "File too large" }` | `size` exceeds `maxFileSize` |
| `415` | `{ "error": "File type not allowed" }` | `type` does not match `allowedTypes` |
| `500` | `{ "error": "Presign failed", "code": "PRESIGN_FAILED" }` | the storage call threw |
Checks run in that order: JSON parse, metadata shape, size, type,
`onBeforeUpload`, then identity resolution. So an oversized file is rejected
with `413` before `getUserId` is consulted.
The `AUTH_REQUIRED` body carries the full remedy as its message:
`Anonymous uploads are disabled. Set allowAnonymousUploads:true, or configure auth/getUserId.`
## `POST /multipart/init`
Starts an S3 multipart session and returns the signed upload token that
authorizes every later step. Same auth gate, same metadata validation, and the
same `400` / `401` / `403` / `413` / `415` table as `/presign`.
**Request**
```ts
{
name: string
type: string
size: number
chunkSizeBytes?: number // requested part size; floors and bumps applied
}
```
**Response `200`** — `MultipartInitResponse` plus the token:
```ts
type MultipartInitResponse = {
key: string
uploadId: string
partSize: number
expiresIn: number
/** Opaque server-issued token bound to {key, uploadId, size, expiry}. */
token?: string
}
```
`token` is always present on responses from this handler. Treat it as opaque:
it is a base64url payload plus an HMAC-SHA-256 signature, and it binds the
object key, the S3 `uploadId`, the resolved user id (`null` when anonymous), the
allowed total-size envelope, an expiry one hour out, and the issue time. Send it
back verbatim on every continuation request.
The issue time is what [`/multipart/resume`](#post-multipartresume) anchors its
window on, and it is carried forward unchanged when that route re-issues a
token. Tokens minted before resume shipped carry no issue time; the resume route
derives it from the expiry, since `init` is the only issuer and has always used
the same TTL.
`partSize` is the server's decision, not your request — see
[Policy and limits](#policy-and-limits).
`500` here is `{ "error": "Multipart init failed", "code": "STORAGE_ERROR" }`.
## `POST /multipart/sign-part`
Presigns one part's `PUT`. The token is the credential — the client never
re-asserts the key or `uploadId`, because the server re-derives both from the
verified token.
**Request**
```ts
{
token: string
partNumber: number
}
```
**Response `200`** — `MultipartSignPartResponse`:
```ts
type MultipartSignPartResponse = {
uploadUrl: string
uploadHeaders?: Record
expiresIn: number
}
```
This handler returns `uploadUrl` and `expiresIn: 3600`.
**Token failures.** A missing, tampered, or stale token returns `403` with the
specific reason, so you can tell a bug from an expiry:
```json
{ "error": "Invalid upload token", "code": "bad_signature" }
```
`code` is one of `malformed`, `bad_signature`, or `expired`. The signature is
verified before any payload byte is trusted, and compared in constant time.
`expired` is recoverable rather than fatal: present the expired token to
[`/multipart/resume`](#post-multipartresume) and retry the call with the fresh
one it returns. upup's own client does this automatically, once per expiry,
shared across its concurrent part uploads.
**Owner binding.** When `getUserId` is configured, the caller's current identity
is re-checked against the `uid` the token was issued to. A token that leaked to a
different authenticated user cannot be replayed:
```json
{
"error": "Upload token does not belong to the current user",
"code": "AUTH_DENIED"
}
```
Without `getUserId` there was no identity to bind at `init`, `uid` is `null`,
and the check is skipped — possession of the token is then the model by design.
**Storage binding.** With a `storage` resolver, the token also carries the
identity of the bucket `init` resolved, and this route answers `403
AUTH_DENIED` if the resolver returns anything else — a continuation cannot be
steered into a different bucket. A static `storage` binds nothing, since there
is only one destination.
`500` is `{ "error": "Multipart sign failed", "code": "STORAGE_ERROR" }`.
## `POST /multipart/complete`
Finalizes the object. Verifies the token and the owner binding exactly as
`sign-part` does, then enforces the signed size envelope before committing.
**Request**
```ts
{
token: string
parts: Array<{ partNumber: number; eTag: string }>
}
```
**Response `200`** — `MultipartCompleteResponse`:
```ts
type MultipartCompleteResponse = {
key: string
publicUrl?: string
downloadUrl?: string
etag?: string
}
```
This handler returns `key`, a signed `downloadUrl` (three days by default, see
`downloadUrlExpiresIn`), and `etag` when S3
supplied one.
**The envelope check.** Because `sign-part` and the browser's direct PUTs never
re-send a size, the real byte total is only knowable here. The handler sums what
S3 actually received (`ListParts`) and compares it against the `[smin, smax]`
range signed at `init`. Outside that range, the multipart upload is **aborted**
— nothing partial is left in the bucket — and the request gets:
```json
{ "error": "Upload size outside signed envelope" }
```
with status `403`. This closes the "declare one byte at init, stream a gigabyte"
path.
After a successful commit, `hooks.onFileUploaded` and `hooks.onUploadComplete`
run. A hook that throws is logged through `onError` and swallowed: the object is
already durable, so the client still gets its `200` rather than a `500` telling
it to retry an upload that succeeded.
`500` is `{ "error": "Multipart complete failed", "code": "STORAGE_ERROR" }`.
## `POST /multipart/abort`
Cancels the session and asks S3 to discard the uploaded parts. Token
verification and owner binding are identical to `sign-part`.
**Request**
```ts
{
token: string
}
```
**Response `200`** — `MultipartAbortResponse`:
```ts
type MultipartAbortResponse = { ok: true }
```
`500` is `{ "error": "Multipart abort failed", "code": "STORAGE_ERROR" }`.
## `POST /multipart/resume`
Re-attaches to an upload a previous page load left in flight. Answers with the
parts storage already holds — each with its byte size — and a freshly-signed
token. This is what powers
[cross-reload resume](/docs/resumable-uploads/#cross-reload-resume), and it is
also how a client refreshes a token that expired mid-upload.
**Request**
```ts
{
token: string
}
```
The token is the only input. The client never sends the key or the `uploadId`,
and neither comes back — the server re-derives both from the verified token, so
the `uploadId` never leaves the signed payload.
**Response `200`** — `MultipartResumeResponse`:
```ts
type MultipartResumeResponse = {
key: string
/** Fresh token replacing the presented one — same key, uploadId, owner,
* and size envelope; new expiry. */
token: string
/** Parts the provider already holds; every entry carries `size`. */
parts: Array<{ partNumber: number; eTag: string; size?: number }>
}
```
Send the returned `token` on every subsequent `sign-part` / `complete` /
`abort` call — the presented one is not invalidated, but it is the stale one.
**Expiry, relaxed and then re-bounded.** This is the only route that accepts a
token whose `exp` has passed, because handing an expired token back as a fresh
one is the whole point: an upload can easily outlive the one-hour TTL. A
tighter bound replaces it — the **resume window**, measured from the original
`/multipart/init` and carried forward unchanged on every re-issue, so a chain of
resumes can never extend it. Signature and shape verification are identical to
every other route.
| Config | Default | Effect |
| --------------------------------- | ------- | -------------------------------------------------------- |
| `multipartResumeWindowSeconds` | `86400` | Seconds after the original `init` that resume is allowed |
| `multipartResumeWindowSeconds: 0` | — | Route is not registered; requests 404 |
A negative or fractional value throws `UpupConfigError` at construction rather
than being coerced.
**Owner binding** is enforced exactly as on `sign-part`: with `getUserId`
configured the caller's identity is re-checked against the token's `uid`, and a
mismatch returns `403 AUTH_DENIED`. Without `getUserId`, possession of the token
is the model, as everywhere else.
**Status codes**
| Status | Body | Cause |
| ------ | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `400` | `{ "error": "Invalid JSON body", "code": "BAD_REQUEST" }` | unparseable body |
| `403` | `{ "error": "Invalid upload token", "code": "malformed" \| "bad_signature" }` | the token is not a token, or the signature does not verify |
| `403` | `{ "error": "Upload resume window has expired", "code": "expired" }` | more than `multipartResumeWindowSeconds` since the original `init` |
| `403` | `{ "error": "…", "code": "AUTH_DENIED" }` | owner mismatch |
| `404` | `{ "error": "Multipart upload no longer exists", "code": "NOT_FOUND" }` | the provider has no such upload — completed, aborted, or reaped by a lifecycle rule |
| `500` | `{ "error": "Multipart resume failed", "code": "STORAGE_ERROR" }` | the storage call threw for any other reason |
A dead upload is deliberately a `404`, never a `500`: upup clients drop the
session and start fresh on a `4xx`, whereas a `5xx` reads as "try again later"
and would retry an upload that can never come back. The `NOT_FOUND` check
matches the provider's `NoSuchUpload` error specifically, not a bare 404 status
— a missing _bucket_ also 404s, and that is a misconfiguration, not a stale
session.
A leaked token stays usable for the resume window instead of one hour. What
it can do with that window is unchanged and narrow: continue the **same**
upload, to the **same** key, inside the **same** signed size envelope, still
owner-bound whenever `getUserId` is configured. It cannot start an upload,
retarget one, or widen the byte envelope. Shorten
`multipartResumeWindowSeconds`, or set it to `0`, if that trade is not one
you want.
## `GET /auth/:provider`
Begins the cloud-drive OAuth flow. Requires a `tokenStore` and a resolvable user
id — drive tokens are stored per user, which is why `providers` or `tokenStore`
without `getUserId` is refused at construction unless you set
`allowAnonymous: true`.
**Query parameters**
| Name | Required | Meaning |
| ---------- | -------- | ------------------------------------------------------------------------ |
| `returnTo` | no | Where to send the popup afterwards. Validated; see the callback section. |
**Response `302`** to the provider's authorization URL, carrying a 32-byte
random `state` and a `redirect_uri` derived from the request URL — always
`/auth/:provider/cb`, so it is identical on the redirect and the exchange
and providers do not reject it as a mismatch. The `state` is stored for **10
minutes** and is single-use.
| Status | Body | Cause |
| ------ | ------------------------------------------------------- | ---------------------------------------- |
| `400` | `{ "error": "Unknown provider: x" }` | not one of the four wire slugs |
| `400` | `{ "error": "Google Drive not configured" }` | that provider is absent from `providers` |
| `401` | `{ "error": "Unauthenticated" }` | `getUserId` returned `null` |
| `500` | `{ "error": "tokenStore is required for OAuth flows" }` | no `tokenStore` configured |
| `500` | `{ "error": "No OAuth providers configured" }` | no `providers` block at all |
Requested scopes are read-only per provider: Google Drive
`drive.readonly`, OneDrive `Files.Read.All offline_access`, Dropbox
`files.content.read files.metadata.read`, Box `root_readonly`.
## `GET /auth/:provider/cb`
The provider's redirect target. Exchanges the authorization code for tokens,
persists them against the user id captured in the `state`, and renders a small
HTML page that closes the popup.
**Query parameters:** `code`, `state`, or `error` from the provider.
**Response `200`** is `text/html`, not JSON. The page posts
`{ type: 'upup:oauth-success', provider }` to `window.opener`, targeted at each
concrete origin in `cors.allowedOrigins` (falling back to `*` only when no
concrete origin is configured — the payload carries no token), then closes
itself. With no opener it redirects to a validated `returnTo`, or prints a
"you may close this window" line.
`returnTo` is validated fail-closed: it is accepted only when it resolves to the
server's own origin or to a concrete (non-wildcard) entry in
`cors.allowedOrigins`. A `*` in the allowlist does not authorize an arbitrary
redirect target.
Both `/auth` routes sit behind your global `auth` gate. The callback arrives
as a top-level browser navigation from the provider, so it carries cookies
but no `Authorization` header — an `auth` implementation that only reads a
bearer token will reject the callback with `401`. Accept a session cookie
there, or scope your gate to the upload routes.
| Status | Body | Cause |
| ------ | --------------------------------------------------------------------- | ------------------------------------------------- |
| `400` | `{ "error": "OAuth error: access_denied" }` | the provider returned an error |
| `400` | `{ "error": "Missing code or state" }` | truncated callback |
| `400` | `{ "error": "Invalid or expired state" }` | replayed, unknown, or provider-mismatched `state` |
| `400` | `{ "error": "Unknown provider: x" }` | bad slug |
| `500` | `{ "error": "tokenStore is required" }` | no `tokenStore` |
| `502` | `{ "error": "Token exchange failed", "code": "AUTH_PROVIDER_ERROR" }` | the provider rejected the code exchange |
## `GET /files/:provider`
Lists one folder of the connected drive, or searches it. Requires a resolved
user id and stored tokens for that user and provider.
**Query parameters**
| Name | Required | Meaning |
| ---------- | -------- | ---------------------------------------------------------- |
| `folderId` | no | Provider folder id; omitted means that provider's root. |
| `search` | no | Free-text query; when present it replaces folder browsing. |
**Response `200`**
```ts
{
provider: 'google-drive' | 'one-drive' | 'dropbox' | 'box'
files: Array<{
id: string
name: string
size?: number
mimeType?: string
thumbnailUrl?: string
isFolder: boolean
modifiedAt?: string
}>
}
```
**Re-authentication.** When the caller has no stored tokens, when a proactive
refresh fails, or when the provider answers `401`, the route returns **`401`**
with a re-auth signal instead of a generic error:
```json
{ "reauth": true, "provider": "google-drive" }
```
In the last two cases the dead tokens are deleted from the store first, so the
next request starts a clean OAuth flow. Send the user back through
`GET /auth/:provider`. Tokens holding a refresh token are refreshed
automatically 30 seconds before expiry, so a routine expiry never surfaces as
`reauth`.
| Status | Body | Cause |
| ------ | -------------------------------------------------------------- | ----------------------------------- |
| `400` | `{ "error": "Unknown provider: x" }` | bad slug |
| `401` | `{ "error": "Unauthenticated" }` | `getUserId` returned `null` |
| `401` | `{ "reauth": true, "provider": "…" }` | no / expired / revoked drive tokens |
| `500` | `{ "error": "tokenStore is required" }` | no `tokenStore` |
| `500` | `{ "error": "Drive request failed", "code": "STORAGE_ERROR" }` | the drive API failed |
## `POST /files/:provider/transfer`
Streams one drive file **server-side** into your bucket, so the bytes never
touch the browser. Same identity, token-refresh, and re-auth behavior as the
list route.
**Request**
```ts
{
fileId: string // required
fileName?: string // overrides the provider's name
size?: number // client-declared; used only for the fast reject
mimeType?: string // checked against allowedTypes
}
```
**Response `200`**
```ts
{
provider: string
key: string
name: string
size: number // ACTUAL bytes transferred, not the declared size
type: string
url: string // three-day signed download URL
}
```
`hooks.onFileUploaded` runs after the object is durable, with the same
throw-is-logged-and-swallowed treatment as multipart complete.
| Status | Body | Cause |
| ------ | -------------------------------------------------------------- | ---------------------------------------------- |
| `400` | `{ "error": "Invalid JSON body" }` | unparseable body |
| `400` | `{ "error": "Missing fileId" }` | no `fileId` |
| `400` | `{ "error": "Unknown provider: x" }` | bad slug |
| `401` | `{ "error": "Unauthenticated" }` | `getUserId` returned `null` |
| `401` | `{ "reauth": true, "provider": "…" }` | no / expired / revoked drive tokens |
| `413` | `{ "error": "File too large" }` | declared `size` exceeds `maxFileSize` |
| `415` | `{ "error": "File type not allowed" }` | `mimeType` fails `allowedTypes` |
| `500` | `{ "error": "tokenStore is required" }` | no `tokenStore` |
| `500` | `{ "error": "Drive request failed", "code": "STORAGE_ERROR" }` | drive fetch, size cap, or storage write failed |
The `413` above is only a cheap early-out on the client-declared size. The
authoritative cap is enforced against the bytes actually streamed — see below.
## Policy and limits
**`maxFileSize`** (bytes) applies to `/presign`, `/multipart/init`, and
`/files/:provider/transfer`. On the upload routes it rejects the declared size
with `413`. On the transfer route it is additionally enforced against real
egress: the transfer aborts the moment the streamed total crosses the cap, the
S3 multipart upload is aborted, and **nothing is persisted**. Omit the option and
no size limit applies.
**`allowedTypes`** is a list of MIME patterns. An exact match passes; a trailing
`/*` matches the whole type family (`image/*` matches `image/png`). An empty or
absent type does **not** match a non-empty allowlist — a missing MIME type is
rejected on both the upload and the transfer path, never waved through. Omit the
option and every type passes.
**Multipart part size.** The S3 floor is `MIN_PART_SIZE` = 5 MiB, and an upload
may have at most 10,000 parts. `partSize` returned by `/multipart/init` is
therefore `max(chunkSizeBytes ?? 5 MiB, 5 MiB, ceil(size / 10000))` — a large
file automatically gets a larger part size instead of failing on the part-count
ceiling. Requesting a chunk below 5 MiB silently floors to 5 MiB.
**Drive-transfer buffering.** `SINGLE_PUT_MAX_BYTES` is 5 MiB. A drive file of
known size at or under it is buffered once and written with a single `PUT`;
anything larger — or of unknown size (a provider-reported size of `0`) —
streams through bounded 5 MiB multipart parts, so peak server memory is one part
regardless of file size. This bound is deliberately not configurable — memory
safety is not a knob that can be raised away.
**Upload-token TTL.** One hour from `init`, reported on `/health` as
`summary.uploadTokenTtlSeconds`. A multipart session that outlives it refreshes
its token through [`/multipart/resume`](#post-multipartresume) rather than
restarting, up to `multipartResumeWindowSeconds` after the original `init`.
**Per-request storage.** When `config.storage` is a resolver rather than a
static object, every route resolves its bucket per request, and the multipart
continuation routes are pinned to the one `init` chose: the token carries a
signed storage identity, the resolver is handed it back as `ctx.storageId`, and
a resolved bucket that does not match answers `403 AUTH_DENIED`. A resolver that
returns an unusable config fails that request with `500 Storage configuration
error` (the real cause goes to `onError` only), and `/health` reports
`checks.storage: "skipped"` with `summary.storageType: "dynamic"`. See
[Multi-bucket routing](/docs/guides/server-mode-setup/#multi-bucket-routing).
**Download-URL TTL.** Three days by default, for every signed `GET` the handler
returns — `downloadUrl` on `/presign` and `/multipart/complete`, `url` on
`/files/:provider/transfer`. Set `downloadUrlExpiresIn` (seconds) to change it.
This is independent of the one-hour upload-URL expiry.
## Rewriting a response body
`hooks.onPresignResponse` is the one seam that can change what a route sends.
It fires on three responses only, discriminated by `ctx.phase`:
| `ctx.phase` | Route | Body it receives |
| --------------------- | --------------------------- | ----------------------------------------- |
| `presign` | `POST /presign` | `PresignedUrlResponse` |
| `multipart-init` | `POST /multipart/init` | `MultipartInitResponse` plus `token` |
| `multipart-sign-part` | `POST /multipart/sign-part` | `MultipartSignPartResponse` |
Returning an object replaces the payload; returning nothing keeps it. The
context is `{ req, phase, key, metadata?, userId }` — `key` is always the key
in the payload, and `metadata` is absent on `multipart-sign-part`, which sees
only a verified token.
The hook runs after auth, policy, and token verification, and after the upload
token is issued. It cannot change a status code, cannot turn a rejection into a
success, and never runs on a request that answered `401`/`403`. It is a body
rewriter for deployments whose storage endpoint the browser cannot reach — see
[Rewriting presign responses](/docs/guides/server-mode-setup/#rewriting-presign-responses).
## Signing a download URL outside the handler
`getDownloadUrl` signs a `GET` for a key that already exists, without going
through any route:
```ts
import { getDownloadUrl } from '@useupup/server'
const url = await getDownloadUrl(config, 'user-42/9f3a/invoice.pdf', {
expiresIn: 300,
})
```
| Argument | Type | Notes |
| ---------------- | --------------------------------- | ------------------------------------------------------------ |
| `config` | `{ storage, downloadUrlExpiresIn?}` | Your `UpupServerConfig`, or any object with a `storage` slice |
| `key` | `string` | The stored object key. Signed as given — not validated |
| `opts.expiresIn` | `number` | Seconds, for this URL only |
Expiry resolves `opts.expiresIn` → `config.downloadUrlExpiresIn` → three days.
Throws `UpupConfigError` when `storage.type` has no S3 API, matching
`createUpupHandler`'s construct-time guard. It performs **no authorization** —
decide whether the caller may read that key before you sign it.
## Adapters
Express, Fastify, Hono, `@useupup/next` (App and Pages routers), and any custom
Node adapter built on `@useupup/server/node-bridge` all dispatch into this exact
contract. The adapters only translate between the framework's request/response
objects and the Web `Request`/`Response` the handler speaks — no route, status
code, or body shape differs between them. See
[Server Mode — Setup](/docs/guides/server-mode-setup/) for the per-framework
mounting code, and [Storage Providers](/docs/guides/storage-providers/) for the
`storage` block behind these routes.
---
# Theme Slots
https://useupup.com/docs/api-reference/upupuploader/classnames/
The stable styling API is `theme.slots`. It is keyed by component, then by the slot within it.
```tsx
```
Use `theme.mode` for color scheme:
```tsx
```
Slot contracts are exported from `@useupup/core/theme`.
## Internals (what actually renders)
Rendered styling comes from `theme.{mode,tokens,slots}` applied through inline
`cn()` class strings inside each component (e.g. `UploaderPanel`). The
`--upup-spacing-*` CSS-variable group is generated by the theme system but is
**not** consumed by any component today — treat it as reserved, not a live
styling knob.
---
# Event Handlers
https://useupup.com/docs/api-reference/upupuploader/event-handlers/
React events mirror core upload state.
```tsx
console.log(files)}
onUploadStart={() => console.log('started')}
onFileUploadComplete={file => console.log(file)}
onUploadComplete={files => console.log(files)}
onError={message => console.error(message)}
/>
```
Common handlers:
| Handler | Called when |
| ----------------------- | ------------------------------------------------------------------------------------- |
| `onFilesSelected` | Files enter core state. |
| `onFileRemoved` | A file is removed. |
| `onUploadStart` | Upload begins. |
| `onFileUploadProgress` | Byte progress changes for one file. |
| `onFilesUploadProgress` | Aggregate byte progress across the batch. |
| `onFileUploadComplete` | One file succeeds. |
| `onUploadComplete` | All files succeed. |
| `onError` | A validation, config, network, or provider failure occurs; delivers a message string. |
File objects use the `UploadFile` contract from `@useupup/core/contracts`.
---
# Icon Prop
https://useupup.com/docs/api-reference/upupuploader/icon-prop/
The icon prop is an object used to configure what icons are shown on the UpupUploader client component. It is optional.
Each icon element must be a valid React element that accepts an optional `className` prop for styling.
Recommended icon library suites include [React
Icons](https://react-icons.github.io/react-icons) and [Lucide
Icons](https://lucide.dev/).
| Key | Example | Type | Default value |
| --------------------------------------------- | -------------------------------------------- | ---------------------------- | ---------------- |
| [CameraCaptureIcon](#cameracaptureicon) | `icons={{CameraCaptureIcon: FaCamera}}` | `FC<{ className?: string }>` | `TbCapture` |
| [CameraDeleteIcon](#cameradeleteicon) | `icons={{CameraDeleteIcon: MdDelete}}` | `FC<{ className?: string }>` | `TbTrash` |
| [CameraRotateIcon](#camerarotateicon) | `icons={{CameraRotateIcon: FaCameraRotate}}` | `FC<{ className?: string }>` | `TbCameraRotate` |
| [ContainerAddMoreIcon](#containeraddmoreicon) | `icons={{ContainerAddMoreIcon: IoAdd}}` | `FC<{ className?: string }>` | `TbPlus` |
| [FileDeleteIcon](#filedeleteicon) | `icons={{FileDeleteIcon: TiDelete}}` | `FC<{ className?: string }>` | `TbTrash` |
| [LoaderIcon](#loadericon) | `icons={{LoaderIcon: TbLoader2}}` | `FC<{ className?: string }>` | `TbLoader` |
## `CameraCaptureIcon`
Custom icon for the camera capture button.
**Example override:**
```tsx
import { FaCamera } from 'react-icons/fa'
;
```
## `CameraDeleteIcon`
Icon for removing captured camera images.
## `CameraRotateIcon`
Button to switch between front/back camera.
## `ContainerAddMoreIcon`
Icon shown in the "Add More Files" button. Appears when multiple uploads are allowed.
## `FileDeleteIcon`
Icon for removing selected files from the list.
## `LoaderIcon`
Animated icon shown during file processing.
**Customization example:**
```tsx
import { ImSpinner8 } from 'react-icons/im'
;
```
All icons inherit the component's dark mode styling automatically through the `className` prop when provided.
---
# Image Editor
https://useupup.com/docs/api-reference/upupuploader/image-editor/
The image editor is **on by default** in React and Preact. Pass
`imageEditor={false}` to opt out, or an options object to configure it.
The image editor ships in `@useupup/react` and `@useupup/preact` only (Preact
loads the real React editor as a lazy island). The Vue, Svelte, Angular, and
Vanilla packages intentionally stub it — a deliberate ruling, not a gap.
```tsx
```
Edited files flow back through core validation before upload.
## Options
```ts
type ImageEditorOptions = {
enabled?: boolean
display?: 'inline' | 'modal'
autoOpen?: 'never' | 'single' | 'always'
output?: {
mimeType?: string
quality?: number
fileName?: (original: File) => string
}
tabs?: (
'Adjust' | 'Annotate' | 'Filters' | 'Finetune' | 'Resize' | 'Watermark'
)[]
tools?: (
| 'Crop'
| 'Rotate'
| 'Flip'
| 'Brightness'
| 'Contrast'
| 'HSV'
| 'Blur'
| 'Text'
| 'Line'
| 'Rect'
| 'Ellipse'
| 'Polygon'
| 'Pen'
| 'Arrow'
| 'Image'
)[]
onOpen?: (file: UploadFile) => void
onCancel?: (file: UploadFile) => void
onSave?: (editedFile: UploadFile, originalFile: UploadFile) => void
}
```
- **`display`** (default `'inline'`) — `'inline'` swaps the editor into the
uploader panel; `'modal'` opens it in a portal over the page.
- **`autoOpen`** (default `'never'`) — `'single'` opens the editor
automatically when exactly one image is selected; `'always'` opens it for
every added image; `'never'` waits for the user to click the edit
affordance.
- **`output`** — controls the saved file: target `mimeType`, encode
`quality`, and a `fileName` callback deriving the edited file's name from
the original.
- **`tabs`** / **`tools`** — restrict which editor tabs and annotation tools
appear; omit either to get the full set.
- **`onOpen` / `onCancel` / `onSave`** — lifecycle callbacks; the same
moments are also emitted as `image-editor-open`, `image-editor-cancel`, and
`image-editor-save` [events](/docs/api-reference/events/).
---
# Optional Props
https://useupup.com/docs/api-reference/upupuploader/optional-props/
Every prop on this page is optional. The three upload-target props (`uploadEndpoint`, `serverUrl`, `resumable.endpoint`) and `provider` are covered in [Required Props](/docs/api-reference/upupuploader/required-props/); the 34 `on*` callbacks are covered in [Events & Callbacks](/docs/api-reference/events/).
Props are identical across React, Vue, Svelte, Angular, Vanilla, Preact, and Next — only `icons` and `style` differ in type per framework. Examples below use React syntax.
## Index
- **Behavior** — [`mode`](#mode), [`autoUpload`](#autoupload), [`quietCompletion`](#quietcompletion), [`enablePaste`](#enablepaste), [`disableDragDrop`](#disabledragdrop), [`folderUpload`](#folderupload), [`onBeforeFileAdded`](#onbeforefileadded)
- **Files & validation** — [`maxFiles`](#maxfiles), [`allowedFileTypes`](#allowedfiletypes), [`maxFileSize`](#maxfilesize), [`minFileSize`](#minfilesize), [`maxTotalFileSize`](#maxtotalfilesize), [`contentDeduplication`](#contentdeduplication)
- **Processing pipeline** — [`imageCompression`](#imagecompression), [`heicConversion`](#heicconversion), [`stripExifData`](#stripexifdata), [`thumbnailGenerator`](#thumbnailgenerator), [`checksumVerification`](#checksumverification), [`webWorker`](#webworker), [`imageEditor`](#imageeditor), [`processingEndpoint`](#processingendpoint), [`processingTimeout`](#processingtimeout)
- **Upload & reliability** — [`maxRetries`](#maxretries), [`maxConcurrentUploads`](#maxconcurrentuploads), [`resumable`](#resumable), [`crashRecovery`](#crashrecovery), [`networkAware`](#networkaware), [`metadata`](#metadata), [`cors`](#cors)
- **Sources & drives** — [`sources`](#sources), [`cloudDrives`](#clouddrives)
- **Appearance & UI** — [`theme`](#theme), [`animations`](#animations), [`mini`](#mini), [`className`](#classname), [`style`](#style), [`icons`](#icons), [`allowPreview`](#allowpreview), [`showBranding`](#showbranding), [`isProcessing`](#isprocessing)
- **Localization** — [`i18n`](#i18n)
- **Headless-only (not component props)** — [`plugins`](#plugins), [`pipeline`](#pipeline), [`workerTimeoutMs`](#workertimeoutms), [`isSuccessfulCall`](#issuccessfulcall)
## Behavior
### `mode`
`mode?: 'client' | 'server'` — default `'client'`, or `'server'` when `serverUrl` is set and `uploadEndpoint` is not.
In both modes the browser PUTs local file bytes straight to storage over presigned URLs. In client mode your own endpoint signs them. In server mode `serverUrl` (`@useupup/server`) signs them, and also runs cloud-drive OAuth, listing, and drive → storage transfers. Pick server mode when cloud-drive OAuth must stay off the browser, or when you want server-side admission (`hooks.onBeforeUpload`), size and type limits, and per-user keys enforced before a URL is issued. See [Client Mode vs Server Mode](/docs/guides/modes/).
```tsx
```
### `autoUpload`
`autoUpload?: boolean` — default `false`.
Starts the upload immediately when files are selected, so the user never presses an upload button. Combine with [`quietCompletion`](#quietcompletion) when the uploader is embedded in a form your app controls.
```tsx
```
### `quietCompletion`
`quietCompletion?: boolean` — default `false`.
When `true`, a successful run shows only a brief checkmark over the panel — no Done button, no summary, no follow-up calls to action. Use it when your app takes over after upload via the completion callbacks or events. The default `false` keeps the normal Done/continue-after-upload flow.
```tsx
```
### `enablePaste`
`enablePaste?: boolean` — default `false`.
Accepts clipboard paste (Ctrl+V / Cmd+V) as a file source. Pasted files run through the same validation, `onBeforeFileAdded` filter, and pipeline as picked files.
```tsx
```
### `disableDragDrop`
`disableDragDrop?: boolean` — default `false`.
Turns off drag-and-drop while keeping the browse/click path fully functional. Useful when the uploader sits inside a surface that owns its own drag behavior (a kanban card, a rich-text editor).
### `folderUpload`
`folderUpload?: { allowDrop?: boolean; showSelectFolderButton?: boolean }` — both default `false`.
`allowDrop` controls directory traversal when a user drops a folder onto the uploader. `showSelectFolderButton` controls whether the My Device source shows an explicit Select folder action.
```tsx
```
### `onBeforeFileAdded`
`onBeforeFileAdded?: (file: File) => boolean | File | undefined | Promise`
An async filter called once per file before it enters the file list. Return `false` to reject the file, a `File` to substitute a different one (renamed, re-wrapped, pre-scrubbed), or `true`/`undefined` to accept it unchanged. It runs before size and type validation, so it is the right place for rules the built-in restrictions cannot express.
```tsx
{
if (file.name.startsWith('~$')) return false
return new File([file], file.name.toLowerCase(), { type: file.type })
}}
/>
```
This is the one file-gating callback documented here because it changes what gets added rather than reporting what happened. Every other `on*` prop lives in [Events & Callbacks](/docs/api-reference/events/).
## Files & validation
### `maxFiles`
`maxFiles?: number` — default `10`.
The maximum number of files that can be in the list at once. Values below `1` are clamped to `1`, and [`mini`](#mini) forces the limit to `1` regardless of what you pass. When the resolved limit is `1` the underlying file input drops its `multiple` attribute. Attempts to exceed the limit surface through `onRestrictionFailed` with reason `LIMIT_EXCEEDED`.
```tsx
```
### `allowedFileTypes`
`allowedFileTypes?: string | string[]` — default `'*'`.
MIME patterns, file extensions, or preset names. Arrays are joined with commas; each comma-separated token is resolved against the preset table, and anything that is not a preset name passes through verbatim, so presets and raw patterns can be mixed freely.
```tsx
```
Preset names available from `@useupup/core`'s `ACCEPT_PRESETS`: `images`, `video`, `audio`, `documents`, `spreadsheets`, `presentations`, `archives`, `code`, `fonts`, `3d`, `design`, `ebooks`, `photography`, `animation`, `ar`, `vector`, `cad`, `gis`, `data`, `markup`, `subtitles`, `email`, `calendar`, `contacts`, `disk`, `ml`, `database`, `certificates`, `firmware`, `executable`, `config`, `financial`, `scientific`, `medical`.
A rejected file fires `onFileTypeMismatch` and `onRestrictionFailed` with reason `TYPE_MISMATCH`.
### `maxFileSize`
`maxFileSize?: { size: number; unit: 'B' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB' }` — default `{ size: 1, unit: 'GB' }`.
Per-file ceiling. Oversized files are rejected before upload and reported through `onRestrictionFailed` with reason `FILE_TOO_LARGE`.
```tsx
```
### `minFileSize`
`minFileSize?: { size: number; unit: 'B' | 'KB' | 'MB' | 'GB' | ... }` — no default (no floor).
Per-file floor, using the same `{ size, unit }` shape. Rejections report reason `FILE_TOO_SMALL`. Useful for catching truncated or zero-byte files before they reach storage.
### `maxTotalFileSize`
`maxTotalFileSize?: { size: number; unit: 'B' | 'KB' | 'MB' | 'GB' | ... }` — no default.
A ceiling on the combined size of everything currently in the list, checked as files are added. This is the right knob for per-submission quotas; `maxFileSize` alone cannot stop fifty acceptable files from adding up.
```tsx
```
### `contentDeduplication`
`contentDeduplication?: boolean` — default `false`.
Hashes each incoming file's bytes (SHA-256 via WebCrypto, with a non-cryptographic fallback where `crypto.subtle` is unavailable) and drops any file whose content already exists in the list. Because it is content-based, it catches the same file added twice under different names — which name-based checks miss.
## Processing pipeline
Pipeline steps run in a fixed order before upload: HEIC conversion, EXIF stripping, compression, thumbnail generation, then hashing. Each step's module is loaded lazily, so a step you do not enable costs nothing in your bundle.
### `imageCompression`
`imageCompression?: boolean` — default `false`.
Re-encodes images before upload. Defaults are quality `0.82` and a longest-edge cap of `1920` pixels. If a target byte size is configured, the step retries in `0.12` quality decrements down to a floor of `0.35` until the output fits. When no size target and no dimension cap are set and the re-encode came out larger than the original, the original file is uploaded untouched.
```tsx
```
The prop is typed as a boolean. To tune `quality`, `maxWidthOrHeight`, or `maxSizeMB`, pass the object form to the engine directly through the headless API — see [Headless Usage](/docs/guides/headless/).
```ts
const core = new UpupCore({
imageCompression: { quality: 0.7, maxWidthOrHeight: 2560, maxSizeMB: 2 },
})
```
### `heicConversion`
`heicConversion?: boolean` — default `false`.
Converts HEIC/HEIF images (the default iPhone camera format) to JPEG so they render in every browser. The decoder (`libheif-js`) is an optional dependency loaded on demand, so enabling this adds nothing to the mandatory bundle. Decode failures surface as a `pipeline-error` diagnostic rather than failing the upload.
### `stripExifData`
`stripExifData?: boolean` — default `false`.
Removes EXIF metadata — including GPS coordinates, device model, and capture timestamps — from images before they leave the browser. Enable it for anything user-generated and publicly served.
### `thumbnailGenerator`
`thumbnailGenerator?: boolean` — default `false`.
Generates a thumbnail for each image and attaches it to the file's `metadata.thumbnailUrl` (plus a `thumbnail.file` blob). Useful when your backend wants a preview it did not have to render itself.
```tsx
```
Like `imageCompression`, the prop is a boolean; the engine option accepts `{ width, height, quality }` for tuning.
### `checksumVerification`
`checksumVerification?: boolean` — default `false`.
Computes a SHA-256 hash of each file and carries it through with the upload so your server can verify the bytes it received match the bytes the browser sent. Worth the cost on large or irreplaceable files; skip it for avatars.
### `webWorker`
`webWorker?: boolean` — unset or `true` means auto, `false` forces the main thread.
Controls whether the pipeline (hash, HEIC, EXIF, thumbnail, compress) runs off the main thread. Auto mode uses a worker where the runtime supports it and falls back to the main thread transparently otherwise, so the UI stays responsive while a 40 MB photo re-encodes. Set `false` only when a host environment forbids workers.
```tsx
```
### `imageEditor`
`imageEditor?: boolean | ImageEditorOptions` — enabled by default; `display` defaults to `'inline'` and `autoOpen` to `'never'`.
Opens a crop/rotate/annotate editor on selected images. Omitting the prop leaves the editor **on** — the heavy editor bundle still loads lazily, only when a user actually opens it. Passing `false` (or `null`) disables it. The object form accepts `enabled`, `display` (`'inline' | 'modal'`), `autoOpen` (`'never' | 'single' | 'always'`), `output`, `tabs`, `tools`, and the `onOpen`/`onCancel`/`onSave` hooks.
```tsx
```
The editor ships in React and Preact only; Vue, Svelte, Angular, and Vanilla accept the prop but stub the UI. See [Image Editor](/docs/api-reference/upupuploader/image-editor/).
### `processingEndpoint`
`processingEndpoint?: string` — no default.
After each file finishes uploading, the uploader opens a Server-Sent Events connection to this URL with the storage key appended as `?key=...`. Use it to wait for server-side work — virus scanning, transcoding, thumbnail rendering — before treating the file as done. The completion event arrives through the `onFileProcessed` callback, documented in [Events & Callbacks](/docs/api-reference/events/).
```tsx
```
### `processingTimeout`
`processingTimeout?: number` — default `60000` (60 seconds).
Maximum milliseconds to wait for the `processingEndpoint` SSE event before closing the connection. Raise it for slow pipelines like video transcoding; the file itself is already uploaded either way.
## Upload & reliability
### `maxRetries`
`maxRetries?: number` — default `3`.
Counts **retries, not attempts**: a file is tried once and then retried up to `maxRetries` times, so the default of `3` means four attempts in total before the file is marked failed. A file rejected by [`isSuccessfulCall`](#issuccessfulcall) is not retried, since a business-logic rejection is unlikely to resolve itself.
```tsx
```
See [Error Handling](/docs/error-handling/) for what reaches your code after retries are exhausted.
### `maxConcurrentUploads`
`maxConcurrentUploads?: number` — default `3`.
How many files upload in parallel. Raising it helps on fast connections with many small files; lowering it to `1` is the safest choice when your presign route is rate-limited or your storage layer meters concurrent writes.
### `resumable`
`resumable?: ResumableUploadOptions` — no default (single-shot uploads).
Multipart and tus are both explicit protocols; there is no automatic selection.
```tsx
```
```tsx
```
The multipart form takes `thresholdBytes`, `chunkSizeBytes`, `persist`, `retryDelays`, `partTimeoutMs`, `maxConcurrentParts`, and `autoResume`. The tus form requires `endpoint` and takes `chunkSizeBytes`, `retryDelays`, `storeFingerprintForResuming`, `removeFingerprintOnSuccess`, `headers`, `metadata`, and `parallelUploads`; `tus-js-client` is an optional dependency loaded on demand. Full protocol details, including every multipart field, are in [Resumable Uploads](/docs/resumable-uploads/).
### `crashRecovery`
`crashRecovery?: boolean` — default `false`.
Persists upload state to IndexedDB so an interrupted session can resume after a page refresh or browser crash. State is saved on every state change while files are present and cleared once the run succeeds. Persistence is best-effort by design: a failed write warns in development and never breaks an upload. A normal unmount leaves the stored state intact, so the session stays recoverable.
```tsx
```
Pair it with `resumable` — recovering the file list is only useful if the transfer itself can pick up where it stopped.
The prop is a boolean. Supplying your own persistence layer instead of IndexedDB is a headless-only option: the engine's `crashRecovery` also accepts an object carrying a custom storage implementation, reachable through `new UpupCore(...)` or `useUpupUpload` — see [Headless Usage](/docs/guides/headless/).
### `networkAware`
`networkAware?: boolean` — default `true`.
Reacts to browser connectivity: going offline mid-upload pauses the run — with multipart `persist` on, that keeps the server-side session alive instead of burning whole-run retries against a dead network — and coming back online resumes it. The resume fires **only** when the pause was offline-initiated; a pause the user chose is never overruled by a connectivity change. A no-op outside a browser.
It is on by default, so the prop is only worth passing to opt out:
```tsx
```
`false` restores the plain fail-and-retry behavior, where a dropped connection burns retries and eventually fails the run.
### `metadata`
`metadata?: Record` — no default.
An arbitrary object sent with presign and multipart-init requests, so your server can attach ownership, tenancy, or routing information to the object it signs. It is the supported way to pass application context through the upload without inventing a side channel.
```tsx
```
### `cors`
`cors?: { dangerouslyAutoConfigure?: boolean; allowedOrigins: string[]; allowedMethods?: string[]; allowedHeaders?: string[]; maxAgeSeconds?: number }`
`allowedOrigins` is required when the object is present. `dangerouslyAutoConfigure` is named for what it does: it can mutate your bucket's CORS policy, so it must be enabled explicitly and scoped to origins you control. Treat it as a local-development convenience and configure CORS on the bucket for production — see [Credentials And CORS](/docs/credentials-configuration/).
```tsx
```
## Sources & drives
### `sources`
`sources?: UploadSource[]` — default `['local', 'url', 'camera', 'microphone', 'screen']`.
Controls which source panels are available and the order they appear in. Cloud drives are **not** in the default set — listing one here also requires configuring it in [`cloudDrives`](#clouddrives) (client mode) or on your `@useupup/server` handler (server mode). `UploadSource` is the string form of the `FileSource` enum — import `FileSource` from `@useupup/core` if you prefer named constants over string literals. Canonical IDs are:
```ts
type UploadSource =
| 'local'
| 'url'
| 'camera'
| 'microphone'
| 'screen'
| 'googleDrive'
| 'oneDrive'
| 'dropbox'
| 'box'
```
```tsx
```
### `cloudDrives`
`cloudDrives?: { googleDrive?: …; oneDrive?: …; dropbox?: …; box?: … }` — no default.
Browser-safe cloud provider configuration for client mode. Google Drive takes `clientId`, `apiKey`, and `appId`; OneDrive, Dropbox, and Box each take `clientId` plus an optional `redirectUri`.
Google Drive also takes an optional `sharedDrives` boolean, default `false`. Left off, the picker lists the signed-in user's own My Drive only — a Drive `files.list` with no corpus set answers from that corpus, so a file living in a shared drive is invisible at every depth. Turn it on and the root of the picker also lists each of the user's shared drives as a folder, plus a "Shared with me" folder, both browsable and downloadable like any other. It needs no extra OAuth scope, and the picker keeps its current shape for everyone who leaves it unset.
```tsx
```
Only publishable values belong here. Use `serverUrl` for OAuth client secrets, token storage, server-side transfers, and compliance workflows — see [Server Mode — Setup](/docs/guides/server-mode-setup/).
## Appearance & UI
### `theme`
`theme?: { mode?: 'light' | 'dark' | 'system'; tokens?: …; slots?: … }`
`theme.mode` supports `light`, `dark`, and `system` (which follows `prefers-color-scheme` and updates live). `theme.tokens` overrides color, radius, shadow, and spacing values; `theme.slots` supplies per-component class overrides, keyed by component and then by the slot within it.
```tsx
```
Full token and slot tables are in [Theming](/docs/guides/theming/).
### `animations`
`animations?: boolean` — default `true`.
Decorative motion (entrance, hover, sheen, and success effects) is on by default. Set `animations={false}` to disable it. `prefers-reduced-motion` is honored automatically regardless of this prop, and essential motion — progress width, focus rings, the spinner — always runs.
```tsx
```
### `mini`
`mini?: boolean` — default `false`.
Renders a compact square uploader (roughly 280 px) instead of the standard fixed-height panel. Mini mode forces a single-file limit: [`maxFiles`](#maxfiles) resolves to `1` and the file input drops `multiple`.
### `className`
`className?: string` — no default.
Additional CSS class applied to the uploader's root container, alongside the required `upup-scope` class. Use it for layout and positioning; use [`theme`](#theme) slots for anything inside the panel.
### `style`
`style?: React.CSSProperties` (React/Preact/Next) or `Record` (Vue, Svelte, Angular, Vanilla) — no default.
Inline styles on the root container. This is one of the two genuinely framework-specific props; the type differs but the DOM result does not.
### `icons`
`icons?: UploaderIcons` — no default.
Replaces individual glyphs with your own components. React accepts `ContainerAddMoreIcon`, `FileDeleteIcon`, `CameraDeleteIcon`, `CameraCaptureIcon`, `CameraRotateIcon`, and `LoaderIcon`, each a component taking `className`. The component type is framework-specific — see [Icon Prop](/docs/api-reference/upupuploader/icon-prop/).
```tsx
```
### `allowPreview`
`allowPreview?: boolean` — default `true`.
Shows an expanded preview for files that can be rendered in the browser (images, video, audio). Set it to `false` for a filename-and-size list — a reasonable choice when users upload sensitive documents on shared screens.
### `showBranding`
`showBranding?: boolean` — default `true`.
Shows or hides the upup branding footer inside the panel.
### `isProcessing`
`isProcessing?: boolean` — default `false`.
A host-controlled busy flag. When `true`, the uploader shows a spinner in the panel corner and stops accepting new files: drag-over, drop, and paste handlers all short-circuit. Drive it from your own async work — form submission, server validation — when the uploader should look busy for a reason the uploader itself does not know about.
## Localization
### `i18n`
`i18n?: { bundle?: LocaleBundle; locale?: LocaleBundle | string; fallbackLocale?: LocaleBundle | string; overrides?: PartialMessages }`
Locale bundles are exported from `@useupup/core/i18n`. Passing a bundle (via `bundle` or `locale`) enables ICU pluralization, namespaced key overrides, and the correct `lang`/`dir` on the root element; `bundle` takes precedence over `locale`. A bare BCP-47 string sets `lang`/`dir` only. `fallbackLocale` fills gaps when the active bundle is missing a key, and `overrides` merges per-key replacements on top of everything else.
```tsx
import { frFR } from '@useupup/core/i18n'
;
```
The full key list and RTL notes are in [Localization](/docs/localization/).
## Headless-only options (UpupCore / useUpupUpload)
These four options are accepted by `UpupCore` but are **not** props on `` — the component's props-to-engine bridge does not forward them, so passing them to the component has no effect. Reach them by constructing the engine yourself with `new UpupCore(...)` or `useUpupUpload` — see [Headless Usage](/docs/guides/headless/).
### `plugins`
`plugins?: UpupPlugin[]` — no default.
Plugins registered at construction. A plugin is `{ name; init?(emitter) }`; `init` receives the core event bus, not the core itself. The built-in cloud-drive plugins are registered for you when you use the component.
### `pipeline`
`pipeline?: PipelineStep[]` — no default.
An explicit pipeline that replaces the automatic one. When you pass it, the boolean flags (`imageCompression`, `heicConversion`, and friends) no longer assemble the step list — you own the order and the contents.
```ts
const core = new UpupCore({
uploadEndpoint: '/api/upload-token',
pipeline: [myWatermarkStep(), compressStep({ quality: 0.7 })],
})
```
### `workerTimeoutMs`
`workerTimeoutMs?: number` — default `30000`.
Per-task timeout for web-worker pipeline work. On timeout the task falls back to main-thread processing; it never fails the file. Raise it if you routinely process very large images on low-end devices.
### `isSuccessfulCall`
`isSuccessfulCall?: (response: { status: number; headers: Record; body: unknown }) => boolean | Promise`
A custom success predicate for the storage response. Use it when a `2xx` is not sufficient proof — for example a gateway that returns `200` with an error body. A file rejected by this predicate is marked failed and is **not** retried, on the assumption a business-logic rejection will not resolve on a second attempt.
## See also
- [Required Props](/docs/api-reference/upupuploader/required-props/) — `uploadEndpoint`, `serverUrl`, `resumable.endpoint`, and `provider`.
- [Events & Callbacks](/docs/api-reference/events/) — all 34 `on*` props.
- [Ref API](/docs/api-reference/upupuploader/ref-api/) — imperative control from the host.
- [Headless Usage](/docs/guides/headless/) — the engine behind the component.
---
# Ref API
https://useupup.com/docs/api-reference/upupuploader/ref-api/
`UpupUploader` exposes the same behavior surface as `useUpupUpload`.
```tsx
const ref = useRef(null)
```
```ts
type UploaderRef = {
useUpload(): {
error?: string
files: UploadFile[]
loading: boolean
progress: number
upload(): Promise
resetState(): void
uploadFiles(
files: File[] | UploadFile[],
): Promise
setFiles(newFiles: File[]): void
replaceFiles(files: File[] | UploadFile[]): void
}
}
```
`UploaderRef` is exported from `@useupup/react`; `UploadFile` is exported from
`@useupup/core/contracts`.
---
# Required Props
https://useupup.com/docs/api-reference/upupuploader/required-props/
`UpupUploader` can run as a local file collector with no upload target. In that case users select files and you read the selected `File` objects from callbacks or hooks.
For actual uploads, configure exactly one target:
| Prop | Type | Use |
| -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `uploadEndpoint` | `string` | Client-hosted presign route. The browser requests an upload URL, then uploads bytes directly to storage. |
| `serverUrl` | `string` | Server-hosted upup route from `@useupup/server`. Your server owns provider calls, storage credentials, and transfer policy. |
| `resumable.endpoint` | `string` | External tus service endpoint when `resumable={{ protocol: 'tus', endpoint }}` is used. |
`provider` is required only when the selected target or server handler needs to know which storage backend to use.
## Client upload target
```tsx
import { UpupUploader } from '@useupup/react'
import '@useupup/react/styles'
export default function Uploader() {
return
}
```
The presign request body includes file details plus optional `metadata`:
```ts
type PresignRequest = {
name: string
type: string
size: number
metadata?: Record
}
```
The response must include the upload URL and may include signed upload headers:
```ts
type PresignedUrlResponse = {
key: string
uploadUrl: string
uploadHeaders?: Record
publicUrl?: string
downloadUrl?: string
expiresIn: number
}
```
## Server upload target
```tsx
```
`serverUrl` should point to a handler created by `@useupup/server`.
---
# Code Examples
https://useupup.com/docs/code-examples/
## Local selection
```tsx
console.log(files)}
/>
```
## Client upload
```tsx
```
Your token endpoint returns upup's presign contract (`key`, `uploadUrl`,
`uploadHeaders`, `expiresIn`). Sign the URL however your storage prefers —
here, AWS S3 via the AWS SDK:
```ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({ region: process.env.S3_REGION! })
export async function POST(req: Request) {
const body = await req.json()
const key = `uploads/${crypto.randomUUID()}/${body.name}`
const uploadUrl = await getSignedUrl(
s3,
new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
ContentType: body.type,
}),
{ expiresIn: 3600 },
)
return Response.json({
key,
uploadUrl,
uploadHeaders: { 'Content-Type': body.type },
expiresIn: 3600,
})
}
```
## Server upload
```tsx
```
## Multipart
```tsx
```
## tus
```tsx
```
---
# Best Angular File Upload Libraries Compared
https://useupup.com/docs/comparisons/best-angular-file-upload-libraries/
**The short answer:** for an Angular uploader that sends files straight to your
own S3-compatible bucket, with cloud drives and resumable uploads, use upup or
Uppy. If your app already uses PrimeNG or DevExtreme, that suite's upload
component is the smallest step. ngx-file-drop only handles the drop zone,
ng2-file-upload adds an upload queue behind your own markup, and FilePond gives
you one polished component that posts to your server.
**Try it first:** the [live Angular demo](/angular/#demo) uploads a real
file from your browser, and the
[Angular quickstart](/docs/quickstarts/angular/) gets it running in your app.
This roundup compares seven Angular file upload libraries on the features that
usually decide the choice, then explains how to pick.
## Comparison table
| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach |
| ----------------------- | ------------- | ------------------------------------ | ------------------------------ | -------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------- |
| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Headless core + native standalone component |
| ngx-file-drop | Yes | No | No | You build it | No | MIT | One drop-zone component; no upload code |
| ng2-file-upload | Yes | No | No | Not built in | No | MIT | Two directives + an upload queue; you build the UI |
| PrimeNG FileUpload | Yes | No | No | Not built in (custom upload handler) | No | v22+: PrimeUI License (paid, with a free community tier); v21 and earlier: MIT | Part of the PrimeNG component suite |
| DevExtreme FileUploader | Yes | No | Chunked uploads to your server | Not built in (custom upload functions) | No | Commercial, per developer (30-day trial) | Part of the DevExtreme component suite |
| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + Angular adapter + plugins |
| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use |
## The libraries
### upup
upup's `@useupup/angular` is a native Angular port of its uploader: a
standalone component with a single `config` input, built on the same headless
core as every other upup package. It uploads directly to S3-compatible storage
with presigned URLs, either from your own signing endpoint or through
`@useupup/server`, and lets users pick files from the device, Google Drive,
OneDrive, Dropbox, Box, a camera, a screen capture or a link. The built-in image
editor is available in the React and Preact packages only, not in Angular. See
the [Angular file uploader](/angular/) page for a three-step setup.
### ngx-file-drop
ngx-file-drop is a drop-zone component that accepts dropped files and folders
and hands them to your code. It does not upload anything, so you add the HTTP
requests, progress and storage yourself. It is a good fit when you only need
the drop target.
### ng2-file-upload
ng2-file-upload, from Valor Software, pairs an upload queue with two
directives. Its `FileUploader` class holds the queue, per-file progress and
filters such as a maximum size or allowed MIME types, and sends each file with
`XMLHttpRequest` to a URL you set. The `ng2FileSelect` and `ng2FileDrop`
directives connect it to a file input and a drop area; the file list, progress
bars and buttons are your own markup. It can send the raw file instead of a
multipart form (`disableMultipart`), which is what an S3 presigned PUT expects,
but it does not sign URLs, split files into chunks or resume uploads. Since
3.0.0, each major version declares a single Angular major as its peer
dependency (9.x for Angular 20; 10.x, on npm's `next` tag, for Angular 21), so
match the version to your Angular release.
### PrimeNG FileUpload
FileUpload is one component in the PrimeNG suite. It offers a basic button mode
and an advanced mode with a drop zone, file list and progress bar, and it posts
to a URL you give it or hands the files to your own upload handler. PrimeNG 21
and earlier are MIT-licensed. Version 22 moved to PrimeTek's PrimeUI License,
which is free for individuals and eligible small organizations, paid
otherwise, and requires a license key. It's the natural choice if PrimeNG is
already your component library.
### DevExtreme FileUploader
FileUploader (`dx-file-uploader`) is one component in DevExpress's DevExtreme
suite. It shows its own drop area, or you can point `dropZone` at another
element. Its `instantly` and `useButtons` upload modes send files with Ajax
requests to `uploadUrl`, and `useForm` submits them with an HTML form. Setting
`chunkSize` splits files into chunks that your server merges (DevExpress
documents ASP.NET and PHP handlers), and the `uploadFile` and `uploadChunk`
functions replace the built-in requests with your own code. S3, cloud drives
and resuming an interrupted upload are not built in; DevExpress publishes
example projects that implement S3 and Azure Blob uploads with `uploadChunk`.
DevExtreme is commercial: the `devextreme-angular` wrapper package is MIT, but
the `devextreme` package it requires needs a paid per-developer license, with a
30-day trial for evaluation. It's the natural choice if DevExtreme is already
your component library. See
[upup vs DevExtreme FileUploader](/docs/comparisons/upup-vs-devextreme-fileuploader/)
for a side-by-side and a migration snippet.
### FilePond
FilePond is a single animated upload component with an Angular adapter. It
posts files to your own server endpoint, supports chunked uploads, and extends
through plugins for previews, validation and image transforms. See
[upup vs FilePond](/docs/comparisons/upup-vs-filepond/).
### Uppy
Uppy is a modular uploader with Angular components. Plugins cover the Dashboard
UI, sources (webcam, screen capture, remote drives through Companion, a server
you host) and destinations such as tus and S3. See
[upup vs Uppy](/docs/comparisons/upup-vs-uppy/).
### Older packages with similar names
Two packages that come up in searches are not compared above. The
`angular-file-uploader` package on npm has had no release since 7.1.1 in
November 2021, and that version declares Angular 10 as its peer dependency.
`ng-file-upload`, whose module is named `ngFileUpload`, is a directive for
AngularJS 1.x, not for current Angular. Its last release, 12.2.13, was in
November 2016, and AngularJS support has officially ended.
## How to choose
- **You want a finished uploader on your own bucket:** upup or Uppy.
- **Your users pick files from Google Drive, OneDrive, Dropbox or Box:** upup
or Uppy. Uppy routes remote files through Companion; upup does it in the
browser (client mode) or in `@useupup/server` (server mode).
- **You already use PrimeNG or DevExtreme:** start with that suite's upload
component and switch only if you need S3, cloud drives or resumable uploads.
- **You only need a drop target:** ngx-file-drop, then write the upload code.
- **You want an upload queue but your own markup:** ng2-file-upload.
- **You need image editing in Angular:** Uppy's Image Editor plugin, or
FilePond's plugins. upup's editor is not available in Angular.
- **You upload very large files:** use resumable uploads, with upup or Uppy.
- **You need permissive licenses throughout:** upup, ngx-file-drop,
ng2-file-upload, FilePond and Uppy are MIT. DevExtreme needs a paid license,
and PrimeNG 22 and later are under the PrimeUI License.
## Minimal upup example
Install the package:
```sh
npm i @useupup/angular
```
Add the standalone component and point it at an endpoint that returns a
presigned URL:
```ts
import { Component } from '@angular/core'
import { UpupUploaderComponent } from '@useupup/angular'
@Component({
selector: 'app-root',
standalone: true,
imports: [UpupUploaderComponent],
template: ``,
})
export class AppComponent {}
```
Load the stylesheet once globally: add `@useupup/angular/styles` to the
`styles` array in `angular.json`, or `@import '@useupup/angular/styles';` in
your global `styles.css`. The [Angular quickstart](/docs/quickstarts/angular/)
covers the endpoint, and [Code Examples](/docs/code-examples/) has a
ready-to-copy presign handler.
## FAQ
### What is the best file upload library for Angular?
For a complete uploader against your own storage, upup or Uppy. Inside a
PrimeNG or DevExtreme app, that suite's upload component. For just a drop zone,
ngx-file-drop, and for an upload queue behind your own markup, ng2-file-upload.
### Which Angular upload libraries upload directly to S3?
upup, with presigned URLs from your endpoint or `@useupup/server`, and Uppy,
with its AWS S3 plugin. With the others you write the S3 signing and upload
code yourself.
### Does upup's Angular package include the image editor?
No. The image editor ships in `@useupup/react` and `@useupup/preact` only.
Everything else, including cloud drives, camera, screen capture and resumable
uploads, works the same in Angular.
### Is upup's Angular component standalone?
Yes. `UpupUploaderComponent` is a standalone component with the selector
`upup-uploader`. Add it to a standalone component's `imports`, or to an
`NgModule`'s `imports`.
### Are these libraries free?
Most are. upup, ngx-file-drop, ng2-file-upload, FilePond and Uppy are
MIT-licensed. PrimeNG is MIT through version 21; version 22 moved to the
PrimeUI License, which is free for individuals and eligible small organizations
and paid otherwise. DevExtreme needs a paid per-developer DevExpress license.
FilePond's full image editor, Pintura, is a separate commercial product.
## Related comparisons
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
- [upup vs DevExtreme FileUploader](/docs/comparisons/upup-vs-devextreme-fileuploader/)
- [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/)
- [Best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/)
---
# Best JavaScript Image Upload Libraries Compared
https://useupup.com/docs/comparisons/best-javascript-image-upload-libraries/
**The short answer:** most JavaScript image upload work is four jobs: show a
preview, shrink the file before it leaves the browser, fix or edit the image,
and send it somewhere. upup and Uppy cover previews, in-browser shrinking and
upload straight to your own S3-compatible bucket: Uppy as separate plugins,
upup as pipeline options, with upup's editor in its React and Preact packages
only. FilePond covers the four jobs with plugins and posts to your own server. Uploadcare does it as a hosted service.
browser-image-compression and Compressor.js do only the shrinking.
**Try it first:** the [live vanilla JS demo](/vanilla/#demo) uploads a real
file from your browser, and the
[Vanilla JS quickstart](/docs/quickstarts/vanilla/) gets it running on your
page. The image options below work the same in the React, Vue, Svelte, Angular
and Preact packages.
This page compares seven options on the image-specific features that usually
decide the choice, then shows how to pick. For the general upload features
(cloud drives, resumable uploads, hosting model), see the
[vanilla JavaScript roundup](/docs/comparisons/best-vanilla-js-file-upload-libraries/).
## Comparison table
| Library | Previews / thumbnails | Resize and compress in the browser | HEIC to JPEG | Crop and edit | Direct-to-S3 | License |
| ------------------------- | -------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------- | ------------------------------------------ | -------------------------- | ----------------------------------- |
| upup | Yes (`thumbnailGenerator`, expanded preview) | Yes (`imageCompression`, on a Web Worker) | Yes (`heicConversion`, optional `libheif-js`) | Yes, React/Preact only | Yes (presigned URLs) | MIT |
| Uppy | Yes (Thumbnail Generator, in Dashboard) | Yes (Compressor plugin) | Not built in; community plugin (last released 2022) | Yes (Image Editor plugin) | Yes (AWS S3 plugin) | MIT |
| FilePond | Yes (image preview plugin) | Yes (image resize and transform plugins) | Not documented | Crop plugin; full editor is Pintura (paid) | Not built in | MIT |
| Dropzone.js | Yes (thumbnails on by default) | Yes (`resizeWidth`, `resizeHeight`) | Not documented | No | Not built in | MIT |
| Uploadcare File Uploader | Yes | Yes (`image-shrink`) | Server-side (accepted as input, converted on Uploadcare's CDN) | Yes (built-in cloud image editor) | No, files go to Uploadcare | MIT library; hosted service is paid |
| browser-image-compression | No UI | Yes (compression only) | Not documented | No | No upload code | MIT |
| Compressor.js | No UI | Yes (compression only) | Not documented | No | No upload code | MIT |
"Not documented" means the library's official docs we read do not mention the
feature, not that it can never be done. Test with your own files.
## What each library does with images
### upup
upup's image handling is a pipeline of opt-in steps that run before the upload
starts, on a Web Worker when one is available:
- **Previews:** `thumbnailGenerator` adds a small JPEG thumbnail (320 px by
default) to each image's metadata as a data URL, and the `allowPreview` prop
(on by default) shows an expanded preview for files the browser can render.
- **Resize and compress:** `imageCompression` caps the longest edge at 1920 px
and re-encodes at quality 0.82 by default. An optional `maxSizeMB` target
retries at lower quality. Animated GIF, WebP and APNG files are skipped so
they keep their frames. See
[client-side image compression](/docs/guides/processing/compression/).
- **HEIC:** `heicConversion` decodes iPhone HEIC and HEIF photos to JPEG with
`libheif-js`, an optional dependency you install yourself. See
[HEIC to JPEG](/docs/guides/processing/heic-conversion/).
- **EXIF:** `stripExifData` removes EXIF metadata, including GPS location, by
re-encoding the image. Compression's own re-encode also drops it when it
replaces the file. Animated GIF, WebP and APNG are skipped, so any EXIF in
them stays; check `metadata.metadataStripSkipped` if that matters.
- **Crop and edit:** the image editor (crop, rotate, flip, annotate, filters,
watermark) ships in `@useupup/react` and `@useupup/preact` only. The vanilla,
Vue, Svelte and Angular packages stub it.
- **Storage:** files go directly to your S3-compatible bucket with presigned
URLs.
### Uppy
Uppy splits image features into plugins. The Dashboard includes the Thumbnail
Generator (200 px wide by default). `@uppy/compressor` wraps Compressor.js; its
default quality is 0.6, which applies to JPEG and WebP images. `@uppy/image-editor`
lets users crop, rotate, zoom and flip, and is built on Cropper.js. The pages
we read do not mention HEIC; a community plugin, `uppy-heic` (last released
in 2022), converts it. Uploads reach S3 through the AWS S3 plugin, which needs a
signing endpoint or Companion, just as upup needs a presign endpoint. See
[upup vs Uppy](/docs/comparisons/upup-vs-uppy/).
### FilePond
FilePond's image features are separate MIT-licensed plugins. The image preview
plugin renders reduced-size previews, and the EXIF orientation plugin lets
previews and transforms correct the rotation phone cameras record in JPEG
metadata. The image transform plugin applies crop and resize edits in the
browser before upload, can output JPEG or PNG, and strips JPEG EXIF data by
default. The image edit plugin connects an editor, including Pintura, a
separate paid product from the same company. FilePond posts to your own server
endpoint and has no built-in S3 signing. See
[upup vs FilePond](/docs/comparisons/upup-vs-filepond/).
### Dropzone.js
Dropzone.js creates image thumbnails by default (120 by 120 px, cropped) and
can resize images before they upload with `resizeWidth`, `resizeHeight`,
`resizeQuality` (0.8 by default) and `resizeMimeType`. It has no editor, and it
posts each file to a URL you configure.
### Uploadcare File Uploader
Uploadcare's File Uploader has a built-in cloud image editor, and an
`image-shrink` option that resizes images in the browser before upload with a
value such as `1600x1600 95%`. It runs on a canvas, so it only handles formats
the browser supports natively, skips images from URL or social sources, and
copies EXIF data over unchanged, including orientation and geolocation. Files
are stored by Uploadcare, which also transforms images on the fly through its
CDN. The service is paid, with a free plan limited to personal use; see their
pricing page for current terms.
### browser-image-compression and Compressor.js
These two are compression libraries, not uploaders. Both are MIT-licensed and
run in the browser.
- **browser-image-compression** takes a `File` and returns a smaller one.
Options include `maxSizeMB`, `maxWidthOrHeight` and `useWebWorker` (on by
default), and `preserveExif` is off by default. It documents JPEG, PNG, WebP
and BMP input.
- **Compressor.js** exposes `quality` (0.8 by default), `maxWidth`,
`maxHeight` and `mimeType`. It reads the EXIF orientation of JPEGs and
rotates them accordingly (`checkOrientation`, on by default), drops EXIF
data unless you set `retainExif`, and returns the original if the result
would be larger (`strict`).
Pair either with your own file input and upload code, or with an uploader that
accepts a pre-processed `File`.
## Looking for a Pintura alternative?
Pintura, from the makers of FilePond, is a paid image editor that offers
cropping with fixed aspect ratios, rotate and flip, filters, color adjustments,
annotation, stickers and watermarks, with FilePond, Dropzone and Uppy
integrations. Check its pricing and license pages for current terms. If you
want an open-source editor instead:
- **Uppy's Image Editor** is MIT-licensed and covers crop, rotate, zoom and
flip.
- **upup's image editor** adds annotation, filters and watermarks, but only in
the React and Preact packages.
- **Uploadcare's editor** is included in its hosted service.
## How to choose
- **You want previews, resizing and an upload to your own bucket from one
library:** upup or Uppy.
- **You need HEIC photos from iPhones to arrive as JPEG:** upup. With the
others, add a separate converter and test it with your own files.
- **You need users to crop or edit before upload in plain JavaScript:** Uppy
or Uploadcare. upup's editor needs its React or Preact package.
- **You already post to your own server endpoint:** FilePond or Dropzone.js.
FilePond has the richer image plugin set.
- **You do not want to run storage:** Uploadcare, then check its pricing page
against your expected volume.
- **You only need smaller files, with your own UI:** browser-image-compression
or Compressor.js.
- **Privacy matters:** decide where EXIF data is removed. upup's
`stripExifData`, Compressor.js's default and FilePond's transform plugin
strip it; Uploadcare's `image-shrink` keeps it. upup skips animated GIF,
WebP and APNG, so EXIF in those stays.
## Minimal upup example
Install the package, plus the optional HEIC decoder if you want HEIC
conversion:
```sh
npm i @useupup/vanilla libheif-js
```
Mount the uploader with the image options switched on:
```ts
import { createUploader } from '@useupup/vanilla'
import '@useupup/vanilla/styles'
const uploader = createUploader('#uploader', {
provider: 'aws',
uploadEndpoint: '/api/upload-token',
imageCompression: true,
heicConversion: true,
thumbnailGenerator: true,
stripExifData: true,
})
```
The [Vanilla JS quickstart](/docs/quickstarts/vanilla/) covers the endpoint
that returns the presigned URL, and
[File Processing](/docs/guides/file-processing/) lists every pipeline step and
its order.
## FAQ
### What is the best JavaScript image upload library?
For a ready-made image uploader that resizes in the browser and sends files to
your own S3-compatible bucket, upup or Uppy. For previews, resizing and
cropping on top of your own server endpoint, FilePond with its image plugins.
For no storage setup, Uploadcare's hosted service. If you only need to shrink
images and already have the UI, browser-image-compression or Compressor.js.
### How do I compress images in the browser before upload?
Re-encode them through a canvas before the request starts. upup does this with
the `imageCompression` option, Uppy with its Compressor plugin, FilePond with
the image transform plugin, and Dropzone.js with `resizeWidth` and
`resizeHeight`. browser-image-compression and Compressor.js do only this step
and leave the uploader to you.
### Which libraries convert HEIC photos to JPEG in the browser?
upup converts HEIC and HEIF to JPEG with the `heicConversion` option, using the
optional `libheif-js` package. None of the other libraries here document a
built-in in-browser HEIC converter. Uppy has a community plugin, `uppy-heic`,
and Uploadcare accepts HEIC and converts it on its CDN after upload, not in the
browser.
### Which image upload libraries include an image editor?
Uppy (Image Editor plugin, crop, rotate, zoom and flip) and Uploadcare (a
built-in cloud image editor) include one. FilePond connects to editors through
its image edit plugin, and the full editor it pairs with, Pintura, is a paid
product. upup's editor covers crop, rotate, flip, annotation, filters and
watermarks, but ships in the React and Preact packages only.
### Are these image upload libraries free?
upup, Uppy, FilePond (including its image plugins), Dropzone.js,
browser-image-compression and Compressor.js are MIT-licensed. Pintura is a
separate paid product. Uploadcare's File Uploader library is open source, but
the hosted service behind it is paid, with a free plan limited to personal
use.
## Related comparisons
- [JavaScript file upload libraries compared](/docs/comparisons/)
- [Best vanilla JavaScript file upload libraries](/docs/comparisons/best-vanilla-js-file-upload-libraries/)
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
- [Client-side image compression](/docs/guides/processing/compression/)
- [Convert HEIC to JPEG in the browser](/docs/guides/processing/heic-conversion/)
---
# Best React File Upload Libraries Compared
https://useupup.com/docs/comparisons/best-react-file-upload-libraries/
**The short answer:** for a complete React uploader that sends files straight to
your own S3-compatible bucket, with cloud drives and resumable uploads, use upup
or Uppy. For a bare drop target you style yourself, use react-dropzone. For
modular hooks, react-uploady. For one polished component, FilePond. For no
storage setup at all, UploadThing's hosted service. If your app already uses
PrimeReact, its FileUpload component is the smallest step.
**Try it first:** the [live React demo](/react/#demo) uploads a real
file from your browser, and the
[React quickstart](/docs/quickstarts/react/) gets it running in your app.
This roundup compares seven React file upload libraries on the features that
decide most projects, then shows how to pick one.
## Comparison table
| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach |
| --------------------- | ------------- | ------------------------------------ | ------------------------------ | ------------------------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- |
| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | Yes | MIT | Headless core + React UI; heavy features load on demand |
| react-dropzone | Yes | No | No | You build it | No | MIT | One small hook; no upload code |
| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use |
| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + React adapter + plugins |
| UploadThing | Yes | Not built in | Yes (since v7) | No, files go to UploadThing | Not built in | MIT SDK; hosted service is paid | SDK packages talking to a hosted service |
| react-uploady | Yes | No | Yes, with its tus package | Not built in | No | MIT | Many small `@rpldy/*` packages; hooks-first |
| PrimeReact FileUpload | Yes | No | No | Not built in (custom upload handler) | No | v11+: PrimeUI License (paid, with a free community tier); v10 and earlier: MIT | Part of the PrimeReact component suite |
## The libraries
### upup
upup is an MIT-licensed uploader with a ready-made React component
(`@useupup/react`) on top of a framework-agnostic core. It uploads directly to
S3-compatible storage with presigned URLs, either from your own signing
endpoint (client mode) or through `@useupup/server` (server mode, which adds
multipart and cloud-drive transfers). Users can pick files from the device,
Google Drive, OneDrive, Dropbox, Box, a camera, a screen capture or a link, and
edit images before upload. Heavy extras such as HEIC conversion and tus are
optional dependencies loaded only when you enable them. See the
[React file uploader](/react/) page for a three-step setup.
### react-dropzone
react-dropzone is a hook, `useDropzone`, that turns any element into a drop
target and returns the accepted and rejected `File` objects. It deliberately
does nothing else: no upload requests, progress or previews. That makes it
tiny and flexible, and makes you responsible for the whole upload path. See
[upup vs react-dropzone](/docs/comparisons/upup-vs-react-dropzone/).
### Uppy
Uppy, from Transloadit, is a modular uploader: a core plus plugins for the
Dashboard UI, sources (webcam, screen capture, remote drives) and destinations
(tus, XHR, S3). Remote sources run through Companion, a Node server you host.
It has React bindings and is the most feature-rich option here. See
[upup vs Uppy](/docs/comparisons/upup-vs-uppy/).
### FilePond
FilePond is a single, carefully animated upload component with a React adapter.
It posts files to your own server endpoint, supports chunked uploads, and
extends through plugins for previews, validation and image transforms. See
[upup vs FilePond](/docs/comparisons/upup-vs-filepond/).
### UploadThing
UploadThing is a hosted upload service with an open-source TypeScript SDK and
React components. You define a file router on your server, and files are
stored on UploadThing's infrastructure. It is the fastest route when you'd
rather not run a bucket. For what it costs next to your own bucket, see
[UploadThing pricing and self-hosted costs](/docs/comparisons/uploadthing-pricing/).
See [UploadThing vs S3](/docs/comparisons/upup-vs-uploadthing/).
### react-uploady
react-uploady is a hooks-first React upload library split into many small
`@rpldy/*` packages: the uploader, a drop zone, previews, chunked uploads,
tus-based resumable uploads and more. You install the pieces you need and build
the UI yourself from its hooks and components.
### PrimeReact FileUpload
FileUpload is one component in the PrimeReact suite. It offers drag and drop,
multiple files, automatic upload, progress, and size and type validation, and
it posts to a URL you give it or, with `customUpload`, hands the files to your
own `uploadHandler`. Version 11 splits it into composable parts such as
`FileUpload.Root` and `FileUpload.Trigger`, plus a headless `useFileUpload`
hook; version 10 is a single component with basic and advanced modes.
PrimeReact 10 and earlier are MIT-licensed. Version 11 moved to PrimeTek's
PrimeUI License, which is free for individuals and eligible small
organizations, paid otherwise, and requires a license key. It's the natural
choice if PrimeReact is already your component library. For the license details
and a migration example, see
[PrimeVue and PrimeReact FileUpload alternatives](/docs/comparisons/primevue-primereact-fileupload-alternatives/).
## How to choose
- **You want it working today, on your own bucket:** upup or Uppy. Both ship a
full UI and upload straight to S3-compatible storage.
- **Your users keep files in Google Drive or Dropbox:** upup or Uppy. Uppy
needs its Companion server; upup's server mode handles drive transfers in
`@useupup/server`, and client mode can fetch drive files in the browser.
- **You have a design system and want to build the UI:** react-dropzone for
selection only, react-uploady for selection plus upload hooks, or upup's
headless core.
- **You already use PrimeReact:** start with PrimeReact FileUpload and switch
only if you need S3, cloud drives or resumable uploads.
- **You don't want to run storage at all:** UploadThing. Check their pricing
page against your expected volume and egress.
- **You upload very large files:** pick a library with resumable uploads: upup,
Uppy, react-uploady (tus) or UploadThing.
- **You need image editing before upload:** upup or Uppy have it built in.
## Minimal upup example
Install the package:
```sh
npm i @useupup/react
```
Render the uploader and point it at an endpoint that returns a presigned URL:
```tsx
'use client'
import { UpupUploader } from '@useupup/react'
import '@useupup/react/styles'
export default function Uploader() {
return
}
```
upup requires React 19. The [React quickstart](/docs/quickstarts/react/) covers
the endpoint, and [Code Examples](/docs/code-examples/) has a ready-to-copy
presign handler. For Next.js, `@useupup/next` bundles the component and the
server handlers; see the [Next.js quickstart](/docs/quickstarts/next/).
## FAQ
### What is the best file upload library for React?
For a complete uploader against your own storage, upup or Uppy. For the
smallest possible building block, react-dropzone. For a hosted service,
UploadThing. Inside a PrimeReact app, PrimeReact FileUpload. The table above
shows the trade-offs.
### Is react-dropzone enough on its own?
Only if you write the rest. react-dropzone handles selecting files; you still
need upload requests, progress, retries, error states and a storage backend.
### Which React upload libraries upload directly to S3?
upup, with presigned URLs from your endpoint or `@useupup/server`, and Uppy,
with its AWS S3 plugin. The others either upload to your server or to a
hosted service, or leave it to you.
### Which React upload libraries support resumable uploads?
upup (tus or S3 multipart), Uppy (tus or S3 multipart), react-uploady (tus)
and UploadThing. FilePond supports chunked uploads to your server.
### Are these libraries free?
upup, react-dropzone, Uppy, FilePond and react-uploady are MIT-licensed.
UploadThing's SDK is MIT, but the service it uploads to is paid with a free
tier; see their pricing page. PrimeReact is MIT through version 10; version 11
moved to the PrimeUI License, which is free for individuals and eligible small
organizations and paid otherwise.
## Related comparisons
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [UploadThing vs S3: a self-hosted alternative](/docs/comparisons/upup-vs-uploadthing/)
- [Best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/)
- [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/)
---
# Best Vanilla JavaScript File Upload Libraries
https://useupup.com/docs/comparisons/best-vanilla-js-file-upload-libraries/
**The short answer:** for a framework-free uploader that sends files straight to
your own S3-compatible bucket, with cloud drives and resumable uploads, use upup
or Uppy. For one polished component that posts to your own server, use FilePond
or Dropzone.js. For no storage setup at all, use Uploadcare's hosted service.
If you only need resumable uploads behind your own UI, use tus-js-client.
**Try it first:** the [live vanilla JS demo](/vanilla/#demo) uploads a real
file from your browser, and the
[Vanilla JS quickstart](/docs/quickstarts/vanilla/) gets it running on your
page.
This roundup compares six JavaScript upload libraries that work without React,
Vue or Angular, then shows how to pick one.
## Comparison table
| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach |
| ------------------------ | ------------- | ------------------------------------ | ------------------------------ | -------------------------- | -------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------ |
| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Framework-free UI on lit-html, plus an `` tag |
| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use |
| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | One component that enhances a file input, plus plugins |
| Dropzone.js | Yes | No | Chunked uploads to your server | Not built in | No (image thumbnails only) | MIT | One library that posts to your server URL |
| Uploadcare File Uploader | Yes | Dropbox, Google Drive and more | Yes (multipart to Uploadcare) | No, files go to Uploadcare | Yes (built-in editor) | MIT library; hosted service is paid | Web components talking to a hosted service |
| tus-js-client | No (no UI) | No | Yes (tus protocol) | No, it needs a tus server | No | MIT | Upload client only; you build the UI |
## The libraries
### upup
upup's `@useupup/vanilla` is the framework-free build of its uploader: the same
UI as the React package, rendered with lit-html. Mount it with
`createUploader('#uploader', options)` or drop an `` custom
element into your HTML. It uploads directly to S3-compatible storage with
presigned URLs, either from your own signing endpoint (client mode) or through
`@useupup/server` (server mode, which adds multipart and cloud-drive
transfers). Users can pick files from the device, Google Drive, OneDrive,
Dropbox, Box, a camera, a screen capture or a link. The image editor is the one
feature it leaves out: it ships in the React and Preact packages only. See the
[vanilla JS file uploader](/vanilla/) page for a three-step setup.
### Uppy
Uppy, from Transloadit, is a modular uploader with a plain JavaScript core:
add plugins for the Dashboard UI, sources (webcam, screen capture, remote
drives) and destinations (tus, XHR, S3). Remote sources run through Companion,
a Node server you host. It is the most feature-rich option here. See
[upup vs Uppy](/docs/comparisons/upup-vs-uppy/).
### FilePond
FilePond is a single, carefully animated upload component written in plain
JavaScript. `FilePond.create()` turns a file input into the widget, which posts
files to your own server endpoint, supports chunked uploads, and extends
through plugins for previews, validation and image transforms. See
[upup vs FilePond](/docs/comparisons/upup-vs-filepond/).
### Dropzone.js
Dropzone.js turns any element into a drop zone with image thumbnails and
progress bars, then posts each file to the URL you configure. It can split
large files into chunks and retry failed chunks, but the server side of that
is yours to write. There are no cloud-drive sources and no direct-to-S3 mode.
### Uploadcare File Uploader
Uploadcare's File Uploader is a set of MIT-licensed web components backed by
Uploadcare's hosted service. Files go to Uploadcare's storage and CDN, with
sources such as Dropbox and Google Drive, resumable multipart uploads and a
built-in image editor. You need an Uploadcare project key, and the service is
paid beyond its free tier; see their pricing page for current numbers.
### tus-js-client
tus-js-client is the reference browser client for the tus resumable upload
protocol. It uploads a file in pieces to any tus server and can resume after a
dropped connection or a page reload. It has no UI at all: you wire it to your
own file input and progress display, and you run the tus server.
### Older libraries you may still find
Fine Uploader and Resumable.js still appear in search results, but neither has
had an npm release in years (Fine Uploader since April 2018, Resumable.js since
November 2017). Prefer a maintained option for new projects.
## How to choose
- **You want it working today, on your own bucket:** upup or Uppy. Both ship a
full UI and upload straight to S3-compatible storage.
- **Your users keep files in Google Drive or Dropbox:** upup, Uppy or
Uploadcare. Uppy needs its Companion server, upup's server mode handles drive
transfers in `@useupup/server`, and Uploadcare does it inside its service.
- **You already have an upload endpoint on your server:** FilePond or
Dropzone.js post straight to it.
- **You don't want to run storage at all:** Uploadcare. Check their pricing
page against your expected volume.
- **You upload very large files:** pick resumable uploads: upup or Uppy (tus or
S3 multipart), Uploadcare, or tus-js-client with your own UI.
- **You need image editing before upload:** Uppy or Uploadcare in plain
JavaScript. upup's editor needs its React or Preact package. For previews,
resizing, HEIC and cropping side by side, see
[best JavaScript image upload libraries](/docs/comparisons/best-javascript-image-upload-libraries/).
## Minimal upup example
Install the package:
```sh
npm i @useupup/vanilla
```
Mount the uploader and point it at an endpoint that returns a presigned URL:
```ts
import { createUploader } from '@useupup/vanilla'
import '@useupup/vanilla/styles'
const uploader = createUploader('#uploader', {
provider: 'aws',
uploadEndpoint: '/api/upload-token',
})
```
Prefer HTML? Import `@useupup/vanilla/element` once, then use the tag:
```html
```
The [Vanilla JS quickstart](/docs/quickstarts/vanilla/) covers the endpoint,
and [Code Examples](/docs/code-examples/) has a ready-to-copy presign handler.
## FAQ
### What is the best JavaScript file upload library without a framework?
For a complete uploader against your own S3-compatible storage, upup or Uppy.
For one polished component that posts to your server, FilePond or Dropzone.js.
For no storage setup at all, Uploadcare's hosted service. For resumable uploads
under your own UI, tus-js-client.
### Can I use upup without React?
Yes. `@useupup/vanilla` mounts the uploader into any element with
`createUploader()`, or declaratively as an `` custom element. It
renders the same UI as the React package, minus the image editor.
### Which vanilla JavaScript upload libraries upload directly to S3?
upup, with presigned URLs from your endpoint or `@useupup/server`, and Uppy,
with its AWS S3 plugin. FilePond and Dropzone.js post to your own server,
Uploadcare uploads to its own storage, and tus-js-client needs a tus server.
### Which of them support resumable uploads?
upup and Uppy (tus or S3 multipart), Uploadcare (multipart uploads to its
service) and tus-js-client (any tus server). FilePond and Dropzone.js upload
large files in chunks to your own server.
### Are these libraries free?
upup, Uppy, FilePond, Dropzone.js and tus-js-client are MIT-licensed.
Uploadcare's File Uploader is MIT, but the service it uploads to is paid with a
free tier. FilePond's full image editor, Pintura, is a separate commercial
product.
## Related comparisons
- [JavaScript file upload libraries compared](/docs/comparisons/)
- [Best JavaScript image upload libraries](/docs/comparisons/best-javascript-image-upload-libraries/)
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
- [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/)
- [Best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/)
- [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/)
---
# Best Vue File Upload Libraries Compared
https://useupup.com/docs/comparisons/best-vue-file-upload-libraries/
**The short answer:** for a Vue 3 uploader that sends files straight to your own
S3-compatible bucket, with cloud drives and resumable uploads, use upup or
Uppy. If your app already uses PrimeVue, its FileUpload component is the
lightest addition. vue-upload-component suits custom UIs, and FilePond gives
you one polished component that posts to your server.
**Try it first:** the [live Vue demo](/vue/#demo) uploads a real
file from your browser, and the
[Vue quickstart](/docs/quickstarts/vue/) gets it running in your app.
This roundup compares five Vue file upload libraries on the features that
usually decide the choice, then explains how to pick.
## Comparison table
| Library | Drag and drop | Cloud drives | Resumable | Direct-to-S3 | Image editing | License | Bundle approach |
| -------------------- | ------------- | ------------------------------------ | ------------------------------ | ------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
| upup | Yes | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | Yes (presigned URLs) | No (the editor is React/Preact only) | MIT | Headless core + native Vue UI; heavy features on demand |
| vue-upload-component | Yes | No | Chunked uploads | Not built in | No | Apache-2.0 | One component; you build the UI around it |
| PrimeVue FileUpload | Yes | No | No | Not built in (custom upload handler) | No | v5+: PrimeUI License (paid, with a free community tier); v4 and earlier: MIT | Part of the PrimeVue component suite |
| FilePond | Yes | No | Chunked uploads to your server | Not built in | Crop/resize plugins; full editor is Pintura (commercial) | MIT | Vanilla JS core + Vue adapter + plugins |
| Uppy | Yes | Many, via Companion | Yes (tus or S3 multipart) | Yes (AWS S3 plugin) | Yes (Image Editor plugin) | MIT | Modular plugins; install only what you use |
## The libraries
### upup
upup's `@useupup/vue` is a native Vue 3 port of its uploader, not a wrapper
around another framework, built on the same headless core as every other upup
package. It uploads directly to S3-compatible storage with presigned URLs,
either from your own signing endpoint or through `@useupup/server`, and lets
users pick files from the device, Google Drive, OneDrive, Dropbox, Box, a
camera, a screen capture or a link. The built-in image editor is available in
the React and Preact packages only, not in Vue. See the
[Vue file uploader](/vue/) page for a three-step setup.
### vue-upload-component
vue-upload-component is a long-standing Vue component that handles file
selection, drag and drop, directories, and chunked uploads to your endpoint.
It renders almost nothing itself, so you design the file list and progress UI.
Check that you install the major version that matches Vue 3.
### PrimeVue FileUpload
FileUpload is one component in the PrimeVue suite. It offers a basic button
mode and an advanced mode with a drop zone, file list and progress bar, and it
posts to a URL you give it or hands the files to your own upload function.
PrimeVue 4 and earlier are MIT-licensed. Version 5 moved to PrimeTek's PrimeUI
License, which is free for individuals and eligible small organizations, paid
otherwise, and requires a license key. It's the natural choice if PrimeVue is
already your component library. For the license details and a migration
example, see
[PrimeVue and PrimeReact FileUpload alternatives](/docs/comparisons/primevue-primereact-fileupload-alternatives/).
### FilePond
FilePond is a single animated upload component with a Vue adapter. It posts
files to your own server endpoint, supports chunked uploads, and extends
through plugins for previews, validation and image transforms. See
[upup vs FilePond](/docs/comparisons/upup-vs-filepond/).
### Uppy
Uppy is a modular uploader with Vue components. Plugins cover the Dashboard UI,
sources (webcam, screen capture, remote drives through Companion, a server you
host) and destinations such as tus and S3. See
[upup vs Uppy](/docs/comparisons/upup-vs-uppy/).
## How to choose
- **You want a finished uploader on your own bucket:** upup or Uppy.
- **Your users pick files from Google Drive, OneDrive, Dropbox or Box:** upup
or Uppy. Uppy routes remote files through Companion; upup does it in the
browser (client mode) or in `@useupup/server` (server mode).
- **You already use PrimeVue:** start with PrimeVue FileUpload and switch only
if you need S3, cloud drives or resumable uploads.
- **You want full control over the markup:** vue-upload-component or upup's
headless core.
- **You need image editing in Vue:** Uppy's Image Editor plugin, or FilePond's
plugins. upup's editor is not available in Vue.
- **You upload very large files:** use resumable uploads, with upup or Uppy.
## Minimal upup example
Install the package:
```sh
npm i @useupup/vue
```
Render the uploader and point it at an endpoint that returns a presigned URL:
```vue
```
upup requires Vue 3.4 or later. The [Vue quickstart](/docs/quickstarts/vue/)
covers the endpoint, and [Code Examples](/docs/code-examples/) has a
ready-to-copy presign handler.
## FAQ
### What is the best file upload library for Vue 3?
For a complete uploader against your own storage, upup or Uppy. Inside a
PrimeVue app, PrimeVue FileUpload. For a custom UI with chunked uploads,
vue-upload-component.
### Which Vue upload libraries upload directly to S3?
upup, with presigned URLs from your endpoint or `@useupup/server`, and Uppy,
with its AWS S3 plugin. With the others you write the S3 signing and upload
code yourself.
### Does upup's Vue package include the image editor?
No. The image editor ships in `@useupup/react` and `@useupup/preact` only.
Everything else, including cloud drives, camera, screen capture and resumable
uploads, works the same in Vue.
### Which Vue upload libraries support resumable uploads?
upup and Uppy support tus and S3 multipart. vue-upload-component and FilePond
support chunked uploads to your own server.
### Are these libraries free?
Most are. upup, FilePond and Uppy are MIT-licensed, and vue-upload-component
is Apache-2.0-licensed. PrimeVue is MIT through version 4; version 5 moved to
the PrimeUI License, which is free for individuals and eligible small
organizations and paid otherwise. FilePond's full image editor, Pintura, is a
separate commercial product.
## Related comparisons
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
- [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/)
- [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/)
---
# JavaScript File Upload Libraries Compared
https://useupup.com/docs/comparisons/
**The short answer:** the main JavaScript file upload options split three ways.
react-dropzone is a file-selection primitive with no upload logic. Uppy, FilePond
and upup are open-source uploaders you run against your own backend. UploadThing
and Uploadcare are hosted services that also store your files. Pick by who owns
the storage and how much UI you want.
This page compares them side by side, then links to a detailed page for each
pairing and to per-framework roundups.
## Feature matrix
| Feature | upup | Uppy | FilePond | UploadThing | react-dropzone | Uploadcare |
| ---------------------- | ----------------------------------------------- | -------------------------------------------- | -------------------------------------------------------- | --------------------------------------------- | --------------------------------- | --------------------------------------- |
| What it is | Uploader library + optional server | Uploader library + optional Companion server | Uploader component | Hosted upload service + SDK | Drag-and-drop selection hook | Hosted file service + uploader widget |
| Ready-made UI | Yes | Yes (Dashboard) | Yes | Yes (button and dropzone) | No, you render it | Yes |
| Frameworks | React, Vue, Svelte, Angular, Preact, vanilla JS | Vanilla JS, React, Vue, Svelte, Angular | Vanilla JS core + React, Vue, Angular, Svelte, jQuery | React, Next.js and other framework adapters | React only | Web components, usable in any framework |
| Where files are stored | Your S3-compatible bucket | Your backend, tus server or S3 bucket | Your server endpoint | UploadThing's infrastructure | Wherever your own code sends them | Uploadcare's storage by default |
| Cloud-drive sources | Google Drive, OneDrive, Dropbox, Box | Many, via Companion | No | Not built in | No | Yes, several |
| Resumable uploads | Yes (tus or S3 multipart) | Yes (tus or S3 multipart) | Chunked uploads to your server | Yes (since v7) | No | Multipart uploads for large files |
| Direct-to-S3 | Yes (presigned URLs, your bucket) | Yes (AWS S3 plugin) | Not built in | No, uploads go to UploadThing | You build it | Not by default; see their docs |
| Image editing | Yes, React/Preact only | Yes (Image Editor plugin) | Crop/resize plugins; full editor is Pintura (commercial) | Not built in | No | Yes |
| License / cost | MIT, free | MIT, free | MIT, free | MIT SDK; the service is paid with a free tier | MIT, free | Paid service with a free tier |
Hosted services change plans and limits often. For UploadThing and Uploadcare,
see their pricing pages for current numbers.
## The three kinds of upload library
### Selection primitives
[react-dropzone](/docs/comparisons/upup-vs-react-dropzone/) gives you a hook
that turns an element into a drop target and hands you `File` objects. Upload
requests, progress, retries, previews and storage are all yours to write. It is
the right choice when you want total control over a small React UI and already
have an upload path.
### Self-hosted uploaders
[Uppy](/docs/comparisons/upup-vs-uppy/),
[FilePond](/docs/comparisons/upup-vs-filepond/) and upup ship a complete
uploader (UI, progress, retries) but send files to storage you control. They
differ mainly in where the UI comes from and how much server you need:
- **Uppy** is a modular toolkit: pick plugins for the Dashboard, sources and
upload destination. Remote sources such as Google Drive go through
Companion, a server you run.
- **FilePond** is a polished single component that posts files to your own
server endpoint, with plugins for previews and image transforms.
- **upup** renders a native component per framework from one headless core,
uploads straight to an S3-compatible bucket with presigned URLs, and ships
`@useupup/server` for signing, multipart and cloud-drive transfers.
### Hosted upload services
[UploadThing](/docs/comparisons/upup-vs-uploadthing/) and Uploadcare run the
storage for you. You trade monthly cost and vendor lock-in for zero bucket
setup. Uploadcare adds a CDN and image transformations on top; UploadThing
focuses on a type-safe upload flow for TypeScript full-stack apps.
## How to choose
1. **Do you need to own the storage?** For compliance, data residency or egress
control, rule out the hosted services and pick a self-hosted uploader.
2. **Which frameworks do you ship?** react-dropzone is React-only. upup, Uppy
and FilePond cover the major frameworks.
3. **Do users pick files from cloud drives?** upup and Uppy support Google
Drive, OneDrive, Dropbox and Box; Uploadcare supports cloud sources as a
hosted feature. FilePond and react-dropzone don't.
4. **How large are the files?** For multi-gigabyte uploads, you need resumable
uploads: tus or S3 multipart in upup and Uppy.
5. **How much UI do you want to build?** None: upup, Uppy, FilePond or a hosted
widget. All of it: react-dropzone.
## Roundups by framework
- [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/):
upup, react-dropzone, Uppy, FilePond, UploadThing, react-uploady and
PrimeReact FileUpload.
- [Best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/):
upup, vue-upload-component, PrimeVue FileUpload, FilePond and Uppy.
- [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/):
upup, ngx-file-drop, ng2-file-upload, PrimeNG FileUpload, DevExtreme
FileUploader, FilePond and Uppy.
- [Best vanilla JavaScript file upload libraries](/docs/comparisons/best-vanilla-js-file-upload-libraries/):
upup, Uppy, FilePond, Dropzone.js, Uploadcare File Uploader and
tus-js-client, with no framework.
- [Best JavaScript image upload libraries](/docs/comparisons/best-javascript-image-upload-libraries/):
previews, client-side resize and compression, HEIC and cropping in upup,
Uppy, FilePond, Dropzone.js, Uploadcare, browser-image-compression and
Compressor.js.
## Head-to-head comparisons
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
- [upup vs react-dropzone](/docs/comparisons/upup-vs-react-dropzone/)
- [upup vs DevExtreme FileUploader](/docs/comparisons/upup-vs-devextreme-fileuploader/)
- [UploadThing vs S3: a self-hosted alternative](/docs/comparisons/upup-vs-uploadthing/)
## Pricing and licensing
- [UploadThing pricing, free tier and S3 costs](/docs/comparisons/uploadthing-pricing/):
the current plans, what "free" covers, and what the same storage costs on S3,
R2, Backblaze B2 and Spaces.
- [PrimeVue and PrimeReact FileUpload alternatives](/docs/comparisons/primevue-primereact-fileupload-alternatives/):
the PrimeUI License change, what FileUpload does, and how to migrate.
## FAQ
### What is the best JavaScript file upload library?
It depends on the job. For a full uploader against your own S3-compatible
storage across several frameworks, upup or Uppy. For one polished component
posting to your own server, FilePond. For a bare React drop target,
react-dropzone. For no storage setup at all, a hosted service such as
UploadThing or Uploadcare.
### Which file upload libraries are free and open source?
upup, Uppy, FilePond and react-dropzone are MIT-licensed. UploadThing's SDK is
MIT but the service it talks to is paid, with a free tier. Uploadcare is a paid
service with a free tier.
### Which libraries upload directly to S3?
upup (presigned URLs, with `@useupup/server` or your own signing endpoint) and
Uppy (its AWS S3 plugin). With FilePond or react-dropzone you write the S3
signing and upload code yourself.
### Which libraries support Google Drive and Dropbox?
upup supports Google Drive, OneDrive, Dropbox and Box. Uppy supports those and
more through its Companion server. Uploadcare supports cloud sources as part of
its hosted service.
## Next steps
- [Quickstart for React](/docs/quickstarts/react/)
- [Client mode vs server mode](/docs/guides/modes/)
- [Storage providers](/docs/guides/storage-providers/)
---
# PrimeVue and PrimeReact FileUpload Alternatives
https://useupup.com/docs/comparisons/primevue-primereact-fileupload-alternatives/
**The short answer:** PrimeVue 5 and PrimeReact 11 are no longer MIT-licensed.
They use PrimeTek's PrimeUI License, with a free Community License for
individuals and eligible small organizations, a paid Commercial License for
everyone else, and a license key in both cases. PrimeVue 4 and PrimeReact 10
and earlier stay MIT. If you want a FileUpload replacement that sends files to
your own S3-compatible bucket, use upup or Uppy. For one component that posts
to your own server, use FilePond.
**Try it first:** the [live Vue demo](/vue/#demo) and the
[live React demo](/react/#demo) upload a real file from your browser, and the
[Vue quickstart](/docs/quickstarts/vue/) and
[React quickstart](/docs/quickstarts/react/) get them running in your app.
This page covers what changed in the licenses, what FileUpload does, which
libraries can replace it in Vue and React, and how to move the code. For the
full roundups, see
[best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/)
and
[best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/).
## What changed in the license
PrimeTek published PrimeVue 5.0.0, PrimeReact 11.0.0 and PrimeNG 22.0.0 to npm
on 15 July 2026 under the PrimeUI License. Earlier major versions kept their MIT
license. The split, by version:
| Library | MIT-licensed versions | PrimeUI License versions |
| ---------- | --------------------- | ------------------------ |
| PrimeVue | 4 and earlier | 5 and later |
| PrimeReact | 10 and earlier | 11 and later |
| PrimeNG | 21 and earlier | 22 and later |
The PrimeUI License lets you choose one of two terms:
- **Community License (free).** For individuals and for organizations that
meet all of these limits: under $1,000,000 in annual gross revenue, fewer than
5 developers, fewer than 10 employees, and under $3,000,000 in venture capital
or outside funding. It covers up to 4 developers and renews each year by
confirming you still qualify. PrimeTek's community-license page also excludes
public-sector bodies and educational institutions.
- **Commercial License (paid).** For organizations that do not qualify. It is
licensed per developer, is perpetual, and includes one year of updates.
Both need a license key. Verification runs offline with no telemetry, and a
missing, invalid or expired key may show a license notice. The package's
`LICENSE.md` and [primeui.dev](https://primeui.dev/licenses/community) set the
exact terms, including how they treat non-profits and public-sector bodies, so
read them against your own situation. This page is not legal advice.
Staying on PrimeVue 4 or PrimeReact 10 keeps you on MIT. Ask PrimeTek how long
each older line will be maintained before you plan around one.
## What FileUpload does
Both libraries ship a component named FileUpload. Neither is an S3 uploader: it
is a form control that selects files and sends them where you tell it.
**PrimeVue FileUpload** (versions 4 and 5 share this API) has two modes. In
`basic` mode it is a button that opens the file dialog. In `advanced` mode, the
default, it adds a drop zone, a file list and a progress bar. Its main props:
- `url` and `name`: where to post the files and under which field name.
- `accept`, `multiple`, `maxFileSize` (in bytes) and `fileLimit`: validation.
- `auto`: upload as soon as files are chosen.
- `customUpload`: skip the built-in request and handle the files yourself.
With `customUpload`, the component fires an `uploader` event whose `files`
property holds the selected files. It also emits `select`, `progress`, `upload`
and `error`.
**PrimeReact FileUpload** in version 10 is one component with the same `mode`,
`url`, `name`, `accept`, `multiple`, `maxFileSize` and `auto` props. With
`customUpload`, it calls your `uploadHandler` with an event whose `files`
property holds the selected files. Callbacks include `onSelect`, `onProgress`,
`onUpload` and `onError`. Version 11 splits the component into composable parts
such as `FileUpload.Root` and `FileUpload.Trigger`, plus a headless
`useFileUpload` hook, and keeps the `url`, `customUpload` and `uploadHandler`
options.
## When to replace it
Keep FileUpload if you already use the PrimeVue or PrimeReact suite, your files
post to your own server, and the license fits you. Replace it when:
- **The license no longer fits.** You are on version 5 or 11 and do not want a
license key or a paid tier, or you want to stay current without the suite.
- **You want files in your own S3-compatible bucket.** FileUpload leaves the
signing, the upload request and retries to you.
- **You need more than a file picker.** Cloud drives, camera, screen capture,
resumable uploads or an image editor are not part of FileUpload.
- **You use one uploader across frameworks.** upup has native ports for React,
Vue, Svelte, Angular, Preact and vanilla JS.
## Alternatives compared
| Library | Framework fit | Where files go | Cloud drives | Resumable | License |
| ------------------------ | --------------------------------------- | --------------------------------------- | ------------------------------------ | ------------------------------ | -------------------------------------------------------- |
| upup | React, Vue, Svelte, Angular, Preact, JS | Your S3-compatible bucket (presigned) | Google Drive, OneDrive, Dropbox, Box | Yes (tus or S3 multipart) | MIT |
| Uppy | Vanilla JS, React, Vue, Svelte, Angular | Your server, a tus server or S3 | Many, via Companion | Yes (tus or S3 multipart) | MIT |
| FilePond | Vanilla JS core with framework adapters | Your server endpoint | No | Chunked uploads to your server | MIT (the Pintura image editor is commercial) |
| vue-upload-component | Vue | Your endpoint | No | Chunked uploads | Apache-2.0 |
| react-dropzone | React | Wherever your own code sends them | No | No | MIT |
| Uploadcare File Uploader | Web components for any framework | Uploadcare's hosted storage | Yes, several | Multipart to Uploadcare | MIT library; the hosted service is paid with a free tier |
| PrimeVue FileUpload | Vue | The `url` you set, or your own function | No | No | v5: PrimeUI License; v4 and earlier: MIT |
| PrimeReact FileUpload | React | The `url` you set, or your own function | No | No | v11: PrimeUI License; v10 and earlier: MIT |
- **upup** is the closest like-for-like swap if you want a ready-made UI and
your own bucket. `@useupup/vue` and `@useupup/react` render a drag-and-drop
panel with previews, progress, retries and validation. The image editor is in
the React and Preact packages only.
- **Uppy** is a modular toolkit with Vue and React components. Remote sources
such as Google Drive go through Companion, a server you run. See
[upup vs Uppy](/docs/comparisons/upup-vs-uppy/).
- **FilePond** is the nearest thing to FileUpload: one polished component that
posts to your server. See
[upup vs FilePond](/docs/comparisons/upup-vs-filepond/).
- **vue-upload-component** handles selection, drag and drop and chunked uploads
but leaves the file list and progress UI to you. Its `latest` npm tag is the
Vue 2 line; install the 3.x line (the `next` tag) for Vue 3.
- **react-dropzone** gives you a drop target and `File` objects, and nothing
else. See [upup vs react-dropzone](/docs/comparisons/upup-vs-react-dropzone/).
- **Uploadcare** is a hosted service. You get its storage and CDN and give up
the bucket.
## Migrate from PrimeVue FileUpload to upup
A typical PrimeVue setup uses `customUpload` and an `uploader` handler that
sends each file to your own code:
```vue
```
With `@useupup/vue`, the component does the upload, so the handler goes away.
Install it with `npm i @useupup/vue`. It needs Vue 3.4 or later.
```vue
```
`upload-endpoint` is a route in your app that returns a presigned upload URL for
each file. The [Vue quickstart](/docs/quickstarts/vue/) and
[Code Examples](/docs/code-examples/) have a ready-to-copy handler.
## Migrate from PrimeReact FileUpload to upup
A typical PrimeReact 10 setup uses `customUpload` and an `uploadHandler`:
```tsx
import { FileUpload, type FileUploadHandlerEvent } from 'primereact/fileupload'
async function uploadHandler(event: FileUploadHandlerEvent) {
for (const file of event.files) {
// your upload code: a FormData POST, or a presigned PUT
}
}
export default function Uploader() {
return (
)
}
```
With `@useupup/react`, again the handler goes away. Install it with
`npm i @useupup/react`. It needs React 19.
```tsx
'use client'
import { UpupUploader } from '@useupup/react'
import '@useupup/react/styles'
export default function Uploader() {
return (
console.log(file.name, key)}
/>
)
}
```
The [React quickstart](/docs/quickstarts/react/) covers the signing route.
## What changes when you switch
- **How bytes travel.** FileUpload posts a form to your `url`. upup asks your
endpoint, or `@useupup/server`, for a presigned URL, and the browser sends
each file straight to the bucket. If your server must receive the file itself,
keep FileUpload or write that route. See
[Client mode vs server mode](/docs/guides/modes/) and the
[storage guides](/docs/guides/storage-providers/).
- **Validation.** PrimeVue and PrimeReact take `maxFileSize` in bytes. upup
takes `{ size, unit }`, plus `maxFiles` and `allowedFileTypes`.
- **Events.** PrimeReact's `onSelect` maps to `onFilesSelected`, `onProgress` to
`onFileUploadProgress` or `onFilesUploadProgress`, and `onUpload` to
`onFileUploadComplete` (one file) or `onUploadComplete` (all files). In Vue,
PrimeVue's `@select` maps to the `onFilesSelected` prop. `onError` receives a
message string. See [Events & callbacks](/docs/api-reference/events/).
- **Appearance.** PrimeVue and PrimeReact theme through their own presets. upup
uses design tokens and slots; see [Theming](/docs/guides/theming/). Its panel
has a fixed height, and the `mini` prop gives a compact square for tight
layouts.
- **The rest of your UI.** upup replaces only the upload component. Dialogs,
tables and forms from PrimeVue or PrimeReact keep working next to it.
## FAQ
### Is PrimeVue FileUpload free?
It depends on the version. PrimeVue 4 and earlier are MIT-licensed. PrimeVue 5
uses the PrimeUI License, which has a free Community License for individuals and
eligible small organizations and a paid Commercial License for everyone else.
Either way, version 5 needs a license key.
### Is PrimeReact FileUpload free?
It depends on the version. PrimeReact 10 and earlier are MIT-licensed.
PrimeReact 11 uses the PrimeUI License, which has a free Community License for
individuals and eligible small organizations and a paid Commercial License for
everyone else. Either way, version 11 needs a license key.
### What is the best alternative to PrimeVue or PrimeReact FileUpload?
For a finished uploader that sends files to your own S3-compatible bucket, upup
or Uppy. For one polished component that posts to your own server, FilePond. For
a bare React drop target you style yourself, react-dropzone. For a Vue component
with no UI of its own, vue-upload-component.
### Can PrimeVue or PrimeReact FileUpload upload straight to S3?
Not out of the box. It posts the files to the url you give it, or, with
`customUpload`, hands them to your own function, where you write the S3 signing
and upload code.
### Do I have to leave PrimeVue or PrimeReact to use upup?
No. upup replaces only the upload component. The rest of your PrimeVue or
PrimeReact UI can stay as it is.
## Related comparisons
- [Best Vue file upload libraries](/docs/comparisons/best-vue-file-upload-libraries/)
- [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/)
- [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/)
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
---
# UploadThing Pricing, Free Tier and S3 Costs
https://useupup.com/docs/comparisons/uploadthing-pricing/
**The short answer:** as of October 2026, UploadThing's free plan gives you 2 GB
of storage and paid plans start at $10 a month for 100 GB. With upup you pay
nothing for the library and only your storage provider's rates for the bytes.
Which is cheaper depends on how much you store and how much your users
download, so this page lists the rates and works three volumes through them.
**Try it first:** the [live React demo](/react/#demo) uploads a real file from
your browser, and the [React quickstart](/docs/quickstarts/react/) gets it
running against your own bucket.
For the feature-by-feature comparison, see
[UploadThing vs S3](/docs/comparisons/upup-vs-uploadthing/). This page covers
money only.
## UploadThing plans and prices
These are the plans on [uploadthing.com/pricing](https://uploadthing.com/pricing),
checked on 10 October 2026. Prices are in US dollars.
| Plan | Price | Storage | Also listed |
| ----------- | ---------------- | ---------------------------------------------- | ---------------------------------------------------------- |
| Free | $0 a month | 2 GB, shared across all your apps | Unlimited uploads and downloads; 7-day audit-log retention |
| 100 GB | $10 a month | 100 GB | Regions, private files; 30-day audit-log retention |
| Usage based | From $25 a month | 250 GB included, then $0.08 per GB over 250 GB | Regions, private files; 30-day audit-log retention |
The page does not publish a separate bandwidth or egress rate for the paid
plans, an overage rate for the 100 GB plan, or file-count and file-size limits
per plan. If any of those matter to you, ask UploadThing before you commit.
Plans change, so treat the numbers above as a snapshot.
## What "free" really means
The free plan is a real $0 plan, and the pricing page says it includes
unlimited uploads and downloads. The catch is the size: 2 GB of storage, shared
across every app on the account. That is enough for a prototype or a small
avatar uploader. It is not enough for video, a document archive or user
galleries.
The pricing page does not spell out what happens when you pass 2 GB. That is
worth a question to UploadThing before a production launch.
UploadThing's SDK is MIT-licensed, which is a separate matter from the hosted
service's price. upup is MIT-licensed too, and has no hosted service, so there
is no plan to outgrow.
## What the self-hosted alternative costs
With upup, files go to a bucket in your own account. The browser sends the bytes
straight to the bucket over a presigned URL that your server (or
`@useupup/server`) signs, so there is no upload service in the middle to pay
for. Your bill has three parts, all from your storage provider:
- **Storage:** bytes at rest, per GB per month.
- **Egress:** bytes your users download, per GB. This is the line that varies
most between providers.
- **Requests:** a small charge per thousand or per million operations on some
providers.
Rates below are the providers' published prices, checked on 10 October 2026.
| Provider (guide) | Storage | Egress (data out) | Requests |
| ---------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| [AWS S3](/docs/guides/storage/aws-s3/) (S3 Standard, US East) | $0.023 per GB-month, first 50 TB | First 100 GB a month free, then $0.09 per GB for the first 10 TB | $0.005 per 1,000 PUT, COPY, POST or LIST; $0.0004 per 1,000 GET |
| [Cloudflare R2](/docs/guides/storage/cloudflare-r2/) (Standard) | $0.015 per GB-month; first 10 GB-month free | None for direct S3 API, Workers and r2.dev access | $4.50 per million Class A; $0.36 per million Class B; first 1M and 10M a month free |
| [Backblaze B2](/docs/guides/storage/backblaze-b2/) | $6.95 per TB-month; first 10 GB free | Free up to 3x your monthly average stored data, then $0.01 per GB | Class A, B and C calls free on pay-as-you-go |
| [DigitalOcean Spaces](/docs/guides/storage/digitalocean-spaces/) | $5 a month includes 250 GiB, then $0.02 per GiB | 1,024 GiB a month included, then $0.01 per GiB | No request charges listed |
| [MinIO](/docs/guides/storage/minio/) (self-hosted) | Your own disks and servers | Your own bandwidth | None |
Sources: [AWS S3 pricing](https://aws.amazon.com/s3/pricing/) and the AWS price
list API for S3 and data transfer, [Cloudflare R2
pricing](https://developers.cloudflare.com/r2/pricing/), [Backblaze B2
pricing](https://www.backblaze.com/cloud-storage/pricing) and [DigitalOcean
Spaces pricing](https://docs.digitalocean.com/products/spaces/details/pricing/).
AWS prices vary by region; the table uses US East (N. Virginia).
## Three volumes, priced
These examples use monthly storage and monthly downloads, rounded, and ignore
request fees. They treat 1 GB and 1 GiB as the same size, which slightly
overstates DigitalOcean's overage. Free allowances are applied where the
provider publishes one.
| Stored / downloaded a month | UploadThing | AWS S3 | Cloudflare R2 | Backblaze B2 | DigitalOcean Spaces |
| --------------------------- | ----------- | --------- | ------------- | ------------ | ------------------- |
| 50 GB / 100 GB | $10.00 | $1.15 | $0.60 | $0.28 | $5.00 |
| 500 GB / 1,000 GB | $45.00 | $92.50 | $7.35 | $3.41 | $10.00 |
| 5,000 GB / 10,000 GB | $405.00 | $1,006.00 | $74.85 | $34.68 | $189.76 |
How to read it:
- **UploadThing** is priced from storage alone, using the cheapest plan that
fits: the 100 GB plan at the first volume, and $25 plus $0.08 per GB over
250 GB after that. The pricing page lists no separate download charge, so none
is added here.
- **AWS S3** is cheap at small volumes because the first 100 GB of egress is
free, and expensive once downloads grow. At 1,000 GB downloaded a month,
egress is $81 of the $92.50.
- **R2 and B2** stay low because egress is free or mostly free. B2's free
egress covers downloads up to three times your stored data, which all three
volumes stay under.
- **Spaces** is a flat $5 until you pass its included storage or transfer.
The formulas, so you can plug in your own numbers:
```text
S3 = storage_GB x 0.023 + max(0, egress_GB - 100) x 0.09
R2 = max(0, storage_GB - 10) x 0.015
B2 = max(0, storage_GB - 10) / 1000 x 6.95
+ max(0, egress_GB - 3 x storage_GB) x 0.01
Spaces = 5 + max(0, storage_GB - 250) x 0.02
+ max(0, egress_GB - 1024) x 0.01
UploadThing = 10 (up to 100 GB), then 25 + max(0, storage_GB - 250) x 0.08
```
## What the table leaves out
- **Requests.** Upload-heavy apps pay per operation on S3 and R2. A million
small uploads is about $5 of PUT requests on S3. On R2, Class A operations
cost $4.50 per million after the first free million a month. A multipart
upload uses several operations per file. B2's pay-as-you-go plan lists those
calls as free.
- **Your time.** UploadThing runs the storage, signing and file URLs. With a
bucket you create it, set its credentials and CORS rule, and run one signing
route. `@useupup/server` is that route, and the storage guides above give you
the exact CORS rule per provider. See
[what you need to go direct to S3](/docs/comparisons/upup-vs-uploadthing/#what-you-need-to-go-direct-to-s3).
- **A CDN or custom domain.** Serving files from your own domain is a separate
setup, and a separate line on the bill, with every provider.
- **Hosted features.** The table prices bytes. It does not price UploadThing's
file router, its dashboard or having someone else run the infrastructure.
## When UploadThing is still the better choice
In the table above, R2, B2 and Spaces cost less than UploadThing at every
volume, and only AWS S3 costs more, once downloads grow. Against a low-egress
provider, UploadThing's case is your time, not your bill. A hosted plan wins
when the numbers are small and your time is not:
- Your total storage is under a few GB. The free plan costs $0, and so do the
first 10 GB on R2 and B2, but with UploadThing there is no bucket to set up.
- You would otherwise spend hours on a bucket, credentials and CORS for a
weekend project.
- You want one invoice and one vendor, with no cloud account to read.
A bucket wins when storage grows past a few hundred GB, when downloads are
heavy, when you already have cloud credits, or when you want the files in an
account you control and a provider you can swap by changing an endpoint.
## Moving from UploadThing to your own bucket
upup is not a drop-in replacement, but the concepts map one to one. The
step-by-step is in
[Migrating from UploadThing to S3 with upup](/docs/comparisons/upup-vs-uploadthing/#migrating-from-uploadthing-to-s3-with-upup).
Copy your existing files to the new bucket before you cancel the plan.
## FAQ
### How much does UploadThing cost?
As of October 2026, UploadThing's pricing page lists three plans in US dollars.
Free is $0 a month for 2 GB of storage shared across all your apps. The 100 GB
plan is $10 a month. The usage-based plan starts at $25 a month with 250 GB
included, then costs $0.08 per GB over 250 GB. Check
[uploadthing.com/pricing](https://uploadthing.com/pricing) for the current
numbers.
### Is UploadThing free?
Partly. The free plan costs $0 a month, with 2 GB of storage shared across all
your apps and unlimited uploads and downloads. The pricing page does not say
what happens when you pass 2 GB, so check with UploadThing before you rely on
it. Beyond 2 GB, the paid plans are the options the page lists.
### Is it cheaper to use your own S3 bucket than UploadThing?
It depends on how much you store and how much your users download. At small
volumes the dollar difference is tiny, so setup time often matters more than
price. At larger volumes,
S3-compatible providers with free or low egress, such as Cloudflare R2 and
Backblaze B2, cost less per GB than UploadThing's overage rate. AWS S3 can cost
more once downloads grow, because it charges for data transferred out.
### Which S3-compatible storage has the cheapest egress?
Cloudflare R2 charges nothing for egress. Backblaze B2 includes free egress up
to three times your stored data each month, then charges $0.01 per GB.
DigitalOcean Spaces includes 1,024 GiB of outbound transfer, then charges $0.01
per GiB. AWS S3 includes 100 GB a month, then charges $0.09 per GB for the first
10 TB.
### Does upup charge anything?
No. upup is MIT-licensed with no hosted tier and no account. You pay only your
storage provider, at that provider's rates.
### What do I take on by leaving UploadThing for my own bucket?
You create and maintain the bucket, its credentials and its CORS rule, and you
run one signing route that issues presigned upload URLs. upup supplies that
route in `@useupup/server`, plus the uploader UI.
## Related comparisons
- [UploadThing vs S3: a self-hosted alternative](/docs/comparisons/upup-vs-uploadthing/)
- [Best React file upload libraries](/docs/comparisons/best-react-file-upload-libraries/)
- [Storage providers](/docs/guides/storage-providers/)
- [Client mode vs server mode](/docs/guides/modes/)
---
# upup vs DevExtreme FileUploader
https://useupup.com/docs/comparisons/upup-vs-devextreme-fileuploader/
**The short answer:** `dx-file-uploader` is a good choice when your Angular app
already uses the DevExtreme suite and your server already accepts its upload
requests. Its documentation covers chunked uploads and S3 and Azure example
projects, but we found no built-in cloud-drive sources or resumable uploads,
and it needs a paid DevExtreme license. upup's `@useupup/angular` is an
MIT-licensed standalone component that uploads straight to an S3-compatible
bucket with presigned URLs and adds cloud-drive sources and resumable uploads.
**Try it first:** the [live Angular demo](/angular/#demo) uploads a real
file from your browser, and the
[Angular quickstart](/docs/quickstarts/angular/) gets it running in your app.
This page compares the two on upload modes, chunking, storage, licensing and
effort to switch, then shows a migration. For other Angular options, see the
[Angular file upload libraries roundup](/docs/comparisons/best-angular-file-upload-libraries/).
## Comparison table
| Feature | DevExtreme FileUploader | upup (`@useupup/angular`) |
| ---------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ |
| What it is | One component in the DevExtreme suite | Standalone uploader component on a headless core |
| Frameworks | Angular, React, Vue and jQuery components | Angular, React, Vue, Svelte, Preact, vanilla JS |
| Upload modes | `instantly`, `useButtons`, `useForm` (HTML form) | Upload button by default; `autoUpload` for instant |
| Where files go | Your `uploadUrl` (POST by default; `uploadMethod`, `uploadHeaders` apply) | Your S3-compatible bucket, via presigned URLs |
| Custom upload code | `uploadFile` and `uploadChunk` functions | Your own endpoint issues the signed URLs |
| Chunked uploads | `chunkSize`; your server must handle chunks | Optional `resumable`: S3 multipart (server mode) or tus |
| Resume after a reload | Not documented | Yes, with multipart or tus |
| Cloud-drive sources | Not documented | Google Drive, OneDrive, Dropbox, Box |
| Camera, screen capture | Not documented | Yes |
| Image editing | Not documented | No in Angular (the editor is React/Preact only) |
| Validation | `accept`, `allowedFileExtensions`, `maxFileSize`, `minFileSize` | `allowedFileTypes`, `maxFileSize`, `minFileSize`, `maxFiles` |
| License | Commercial, per developer, 30-day trial | MIT |
DevExtreme's documentation says its `accept` option filters the file dialog,
and that `allowedFileExtensions` prevents prohibited types from uploading but
does not restrict the dialog, so you set both to get both effects. upup's `allowedFileTypes` does both: it sets the
file input's accept filter and rejects other files as they are added.
## What dx-file-uploader gives you
`dx-file-uploader` (in `devextreme-angular`) shows a select button, a drop area
and a file list. In `instantly` mode (the default) it uploads as soon as files
are chosen; `useButtons` waits for the Upload button; `useForm` leaves the
upload to an HTML form submit. The first two send Ajax requests to `uploadUrl`.
Setting `chunkSize` splits files into chunks, which your server has to
reassemble, and `uploadFile` and `uploadChunk` replace the built-in requests
with your own code. It fits naturally next to DevExtreme's grids and forms.
What it does not include is a storage story. To reach S3 you write the signing
and upload code in `uploadChunk` or `uploadFile` (DevExpress publishes example
projects for S3 and Azure Blob). Its documentation lists no cloud-drive
sources, camera or screen capture, and does not document resuming an
interrupted upload.
## What upup gives you in Angular
`@useupup/angular` exports a standalone `UpupUploaderComponent` (selector
`upup-uploader`) that takes a single `config` input and emits `filesAdded`,
`fileRemoved`, `uploadProgress`, `uploadAllComplete` and `error`. It works with
Angular 19 to 22. In client mode your server only issues short-lived presigned
URLs, and the browser uploads straight to the bucket. Users can pick files from
the device, Google Drive, OneDrive, Dropbox, Box, a camera, a screen capture or
a link. Large files can use S3 multipart (through `@useupup/server`) or tus, and
both resume after a reload. The image editor is the one feature Angular does
not get; it ships in the React and Preact packages only.
## Licensing
DevExtreme is licensed per developer. DevExpress states that building
commercial applications requires a 12-month subscription, that a 30-day trial
is available, and that a valid license key is used to avoid a verification
message in the browser console. The `devextreme-angular` wrapper package is
MIT-licensed, but it requires the `devextreme` package, which needs the paid
license. DevExpress's
current terms are on its [licensing page](https://js.devexpress.com/Licensing/);
check them against your team size and renewal plans.
upup is MIT-licensed end to end: the Angular package, the core and
`@useupup/server`.
## Migrate from dx-file-uploader to upup
Take a typical DevExtreme setup:
```html
```
The upup version replaces the component, the options and the callback. Install
the package first:
```sh
npm i @useupup/angular
```
```ts
import { Component } from '@angular/core'
import { UpupUploaderComponent } from '@useupup/angular'
import type { UploadFile, UploaderProps } from '@useupup/angular'
@Component({
selector: 'app-uploader',
standalone: true,
imports: [UpupUploaderComponent],
template: ``,
})
export class UploaderComponent {
config: UploaderProps = {
provider: 'aws',
uploadEndpoint: '/api/upload-token',
allowedFileTypes: 'image/*',
maxFiles: 10,
maxFileSize: { size: 32, unit: 'MB' },
}
onAllUploaded(files: UploadFile[]) {
console.log('uploaded', files.length)
}
}
```
Load the stylesheet once globally, as described in the
[Angular quickstart](/docs/quickstarts/angular/).
How the options map:
| DevExtreme | upup |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `uploadMode="instantly"` | `autoUpload: true` |
| `uploadMode="useButtons"` | The default; the user presses upload |
| `uploadMode="useForm"` | No equivalent |
| `accept`, `allowedFileExtensions` | `allowedFileTypes`: MIME patterns, extensions or preset names such as `images` |
| `multiple` (default `false`) | `maxFiles` (default 10); set `maxFiles: 1` for a single file |
| `maxFileSize`, `minFileSize` (bytes) | `maxFileSize`, `minFileSize` as `{ size, unit }` |
| `uploadUrl`, `uploadMethod`, `uploadHeaders` | `uploadEndpoint` (see the note below) |
| `chunkSize` | `resumable` with `protocol: 'tus'` (needs a tus endpoint), or `protocol: 'multipart'` (needs `serverUrl` from `@useupup/server`, not `uploadEndpoint`) |
| `onUploaded`, `onUploadError` | `(uploadAllComplete)`, `(error)` outputs |
| `onValueChanged` | `(filesAdded)` and `(fileRemoved)` outputs |
| `labelText`, `selectButtonText` | `i18n` overrides; see [Localization](/docs/localization/) |
The one change that is not a rename: `uploadUrl` receives the file, while
`uploadEndpoint` does not. Your endpoint returns a presigned URL for the
bucket and the browser uploads the file there. The
[Angular quickstart](/docs/quickstarts/angular/) and
[Code Examples](/docs/code-examples/) include a presign handler to copy. If you
used `uploadFile` or `uploadChunk` to reach S3 yourself, that code can go: the
signing now lives in that endpoint or in `@useupup/server`. For multipart
uploads, see [Resumable uploads](/docs/resumable-uploads/).
## When to keep dx-file-uploader
Keep it if your app is built on DevExtreme, your server already handles its
upload requests, and you do not need S3, cloud drives or resumable uploads.
Moving buys little there. Move to upup when files should land in your own
bucket without passing through an application server, when users pick files
from Google Drive or Dropbox, when large uploads must survive a dropped
connection, or when you do not want a per-developer license for an upload box.
## FAQ
### Is dx-file-uploader free?
No. DevExtreme is commercial software licensed per developer, with a 30-day
trial. The `devextreme-angular` wrapper package is MIT, but it requires the
`devextreme` package, which needs the paid license. upup's `@useupup/angular` is
MIT-licensed.
### Can dx-file-uploader upload directly to S3?
Not out of the box. It sends files to the `uploadUrl` you set. You can replace
the built-in requests with your own `uploadFile` or `uploadChunk` function, and
DevExpress publishes example projects for S3 and Azure Blob. upup signs
presigned URLs and the browser uploads straight to your bucket.
### How do I replace dx-file-uploader with upup in Angular?
Install `@useupup/angular`, import the standalone `UpupUploaderComponent`, and
pass one `config` object. Map `uploadMode` to `autoUpload`, `accept` to
`allowedFileTypes`, `multiple` to `maxFiles`, and `maxFileSize` to a size and
unit object. Point `uploadEndpoint` at an endpoint that returns a presigned URL
instead of one that receives the file.
### Does upup have the same upload modes as dx-file-uploader?
Two of three. upup's default is the `useButtons` behavior, where the user
presses upload, and setting `autoUpload` to true matches `instantly`. There is no
equivalent of `useForm`, which submits files with an HTML form.
### When should I keep dx-file-uploader?
When your app already uses DevExtreme, your server already handles its upload
requests, and you do not need S3, cloud drives or resumable uploads. Switching
buys little in that case.
## Related comparisons
- [Best Angular file upload libraries](/docs/comparisons/best-angular-file-upload-libraries/)
- [JavaScript file upload libraries compared](/docs/comparisons/)
- [upup vs Uppy](/docs/comparisons/upup-vs-uppy/)
- [upup vs FilePond](/docs/comparisons/upup-vs-filepond/)
---
# upup vs FilePond
https://useupup.com/docs/comparisons/upup-vs-filepond/
FilePond (by PQINA) is a polished, highly accessible JavaScript uploader known for
its silky image-optimization UX and a broad catalog of plugins, with first-party
adapters for React, Vue, Angular, Svelte, and jQuery. It renders its own UI
component and focuses on uploading local files and URLs to your own server
endpoint. It does not include cloud-drive browsing, camera, or screen capture out
of the box, and its full-featured image editing is a separate product (Pintura).
upup is a headless core plus native UI that adds cloud drives, camera and screen
capture, and an S3-compatible server with an HMAC-signed trust model.
## At a glance
| Feature | FilePond | upup |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| Native first-party UI | Vanilla JS core + adapters: React, Vue, Angular, Svelte, jQuery | React, Vue, Svelte, Angular, Vanilla JS, Preact |
| Headless core | No — renders its own UI component | Yes (`@useupup/core`) |
| License / pricing | MIT, free & open source (the Pintura image editor is a separate commercial product) | MIT, free & open source |
| Self-host incl. S3-compatible | Yes — uploads to your own server endpoint (process / revert / restore), with chunk uploads; no built-in S3 signing | Yes — `@useupup/server` signs direct uploads to any S3-compatible storage |
| Cloud-drive sources | No (local files, directories, blobs, local/remote URLs, Data URIs, paste) | Google Drive, OneDrive, Dropbox, Box |
| Camera / screen capture | No | Yes (both) |
| Image editor | Via plugins (crop / resize / transform); a full editor is Pintura (separate/commercial) | Yes — React/Preact only |
| Resumable uploads | Chunked uploads (server-driven) | Yes — optional (tus or S3 multipart) |
| i18n | 48 locale files in `locale/`, plus configurable label strings | Yes (ICU locale bundles) |
Looking for a Pintura alternative? See
[best JavaScript image upload libraries](/docs/comparisons/best-javascript-image-upload-libraries/#looking-for-a-pintura-alternative).
## Choose FilePond if
- You want best-in-class image preview and optimization UX, smooth animations, and
strong accessibility for a local-file upload flow.
- You need a jQuery adapter, or you're happy uploading to your own server endpoint.
- You want a mature, plugin-rich, single-purpose uploader and will add any cloud
sources yourself.
## Choose upup if
- You need cloud-drive sources (Google Drive, OneDrive, Dropbox, Box), camera, or
screen capture built in.
- You want a headless core you can drive from your own UI, plus native UI across
six frameworks with a shared DOM contract.
- You want an included server that signs direct uploads to any S3-compatible
storage behind an HMAC-signed trust model.
---
# upup vs react-dropzone
https://useupup.com/docs/comparisons/upup-vs-react-dropzone/
react-dropzone is a small, focused React hook (`useDropzone`) — with a matching
wrapper component — for building an HTML5-compliant drag-and-drop file-selection
zone. It is a **primitive, not a full uploader**: it hands you the selected or
dropped `File` objects and leaves the upload, UI, progress, previews, cloud
sources, and everything else to you. That's exactly what you want when you only
need drag-and-drop in React and will build the rest yourself. upup is a full
uploader — a headless core, native UI for six frameworks, and an optional server —
so the two really solve different problems. This page is here because teams often
start with the primitive and later need the whole flow.
## At a glance
| Feature | react-dropzone | upup |
| ----------------------- | --------------------------------------------------------- | ------------------------------------------------------------------- |
| Scope | Drag-and-drop file-selection primitive (hook + component) | Full uploader: selection, UI, upload pipeline, and server |
| Native first-party UI | No — you render your own; React only | React, Vue, Svelte, Angular, Vanilla JS, Preact |
| Headless core | Yes (a hook) — but file selection only, no uploading | Yes (`@useupup/core`) — a full upload engine |
| License / pricing | MIT, free & open source | MIT, free & open source |
| Uploading / self-host | You implement uploads yourself, to any backend | `@useupup/server` signs direct uploads to any S3-compatible storage |
| Cloud-drive sources | No | Google Drive, OneDrive, Dropbox, Box |
| Camera / screen capture | No | Yes (both) |
| Image editor | No | Yes — React/Preact only |
| Resumable uploads | No | Yes — optional (tus or S3 multipart) |
| i18n | No | Yes (ICU locale bundles) |
## Choose react-dropzone if
- You only need a drag-and-drop zone in React and will build the upload, UI, and
progress yourself.
- You want the smallest, most unopinionated primitive, with no upload logic baked
in.
- You already have an upload backend and UI and just need clean file selection.
## Choose upup if
- You want the drag-and-drop **and** the progress bar, previews, cloud drives,
camera, resumable uploads, and a server — without assembling them yourself.
- You need more than React: Vue, Svelte, Angular, Vanilla JS, and Preact, all
sharing one DOM contract.
- You'd rather configure a complete uploader than build one from a primitive.
---
# UploadThing vs S3: A Self-Hosted Alternative
https://useupup.com/docs/comparisons/upup-vs-uploadthing/
**The short answer:** UploadThing is a hosted upload service that stores your
files in infrastructure it runs. An S3 bucket is storage you own and pay for
directly, but uploading straight to S3 means building presigned URLs, CORS, an
upload widget and multipart handling yourself. upup is the MIT-licensed library
that supplies those pieces.
This page compares the two models honestly, lists what "direct to S3" actually
requires, and shows the migration path with upup's real server API.
## What UploadThing is
UploadThing (by Ping Labs) is a hosted file-upload service for full-stack
TypeScript apps. You declare a **file router** on your server (what each
endpoint accepts, who may call it, and what runs when an upload finishes),
install its open-source SDK, and render its `UploadButton` or `UploadDropzone`
React components, or build your own UI with its hooks. Adapters cover Next.js
and other full-stack frameworks.
It is, in effect, a managed layer over object storage. UploadThing runs the
storage, the request signing and the file URLs, so there is no bucket, IAM
policy or CORS rule for you to manage. The SDK is MIT-licensed. The service has
a free tier and paid plans; see [Is UploadThing free?](#is-uploadthing-free)
below.
## UploadThing vs your own S3 bucket
The question behind "UploadThing vs S3" is not really which storage is better.
It is whether you want a service to own the upload layer, or want the files in
an account you control.
| Concern | UploadThing | Your own S3 bucket |
| -------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Where the files live | Storage that UploadThing operates | A bucket in your own cloud account (or your own MinIO) |
| Who you pay | UploadThing, per its plans (see their pricing page) | Your storage provider, at its storage, request and transfer rates |
| Egress (downloads) | Governed by UploadThing's plan terms | Billed by your provider; some S3-compatible providers do not charge for it |
| File URLs you store | Point at UploadThing's domains | Point at your bucket, your CDN or your own domain |
| Region and residency | Chosen by the service | You pick the region, or run storage on-premises |
| Switching away later | Export the files and rewrite every stored URL | The S3 API is implemented by many providers, so a move is mostly an endpoint change |
| Setup work | Minimal: an API key and a file router | Bucket, credentials and CORS, plus the upload plumbing described below |
### Ownership
With UploadThing, your users' files sit in an account that is not yours. That is
the point of a managed service, and for many apps it is the right call. With
your own bucket, access policies, encryption settings, lifecycle rules, backups
and audit logs are all yours to configure with your provider's own tooling.
### Cost model
UploadThing bundles storage, signing and delivery into one plan. S3 bills the
parts separately: stored bytes, requests, and data transfer out. Neither is
universally cheaper. A bucket tends to win when you already have cloud credits
or a committed-spend agreement, store a lot of data, or want to pick a provider
per workload. A plan tends to win when your volume is small and you value not
running anything. Price both against your own numbers; don't take anyone's
headline figure, including this page's (it has none on purpose). For the
current plan prices and three worked storage-and-download volumes, see
[UploadThing pricing and self-hosted costs](/docs/comparisons/uploadthing-pricing/).
### Egress
Egress, the bytes your users download, is where object-storage bills surprise
people. AWS S3 charges for data transferred out to the internet. S3-compatible
alternatives differ: Cloudflare R2 does not charge egress fees, and self-hosted
MinIO has no vendor egress bill at all, only your own bandwidth. Because upup
speaks the S3 API, choosing a provider for its egress pricing doesn't change
your upload code.
### Lock-in
Moving off a hosted upload service means copying every file out and rewriting
every URL your database stores. Moving between S3-compatible providers means
changing an endpoint and credentials, then copying objects with standard S3
tooling. Your upload code stays put.
## What you need to go direct to S3
Uploading from a browser straight to a bucket, without routing every byte
through your server, takes four pieces. Here is what each one is and which part
of upup covers it.
### 1. Presigned URLs
The browser must never hold your storage credentials, so your server signs a
short-lived URL for exactly one object, and the browser `PUT`s the bytes to it.
- **Client mode:** point `uploadEndpoint` at your own route. It returns upup's
presign contract (`key`, `uploadUrl`, optional `uploadHeaders`, `expiresIn`);
see [S3 Presign Responses](/docs/api-reference/s3-generate-presigned-url/).
- **Server mode:** `@useupup/server`'s `createUpupHandler` answers `POST
/presign` for you. The server chooses the object key, so a client can never
write to a key it made up, and the upload routes stay closed (`403
AUTH_REQUIRED`) until you configure `getUserId`, `auth`, or the explicit
`allowAnonymousUploads` opt-in. See
[Server Auth & Trust Model](/docs/guides/server-auth/).
### 2. CORS on the bucket
Whenever the browser `PUT`s to the bucket itself, the bucket must allow your
origin, the `PUT` method and the headers the signed request carries, and it
must expose `ETag` so multipart uploads can complete. Each storage guide has
the exact rule: [AWS S3](/docs/guides/storage/aws-s3/),
[Cloudflare R2](/docs/guides/storage/cloudflare-r2/),
[MinIO](/docs/guides/storage/minio/).
### 3. An upload widget
A dropzone is the easy part. Previews, per-file progress, retries, cancel,
file-type and size limits, accessibility and i18n are the parts that take
weeks. `UpupUploader` ships all of that as native components for React, Vue,
Svelte, Angular, Preact and vanilla JS, plus Google Drive, OneDrive, Dropbox and
Box sources, camera and screen capture. See
[Sources](/docs/guides/sources/).
### 4. Resumable multipart uploads
A single `PUT` fails badly on a 2 GB video over hotel Wi-Fi. S3's answer is
multipart upload, which needs an init call, a signed URL per part, a complete
call, and cleanup. Set `resumable: { protocol: 'multipart' }` in server mode and
upup runs that lifecycle against `@useupup/server`, using an HMAC-signed token
that binds the object key, the S3 `uploadId` and a size envelope. An upload
interrupted by a reload continues from the parts storage already holds. See
[Resumable uploads](/docs/resumable-uploads/).
## Feature comparison
| Feature | UploadThing | upup |
| ----------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Model | Hosted service (managed storage and file URLs) | Self-hosted library (your storage) |
| License / pricing | MIT SDK; hosted service with a free 2 GB tier and paid plans | MIT, free; you pay only your storage provider |
| Storage | UploadThing's infrastructure | Any S3-compatible bucket: AWS, R2, MinIO, Spaces, Wasabi, Backblaze B2 and more |
| First-party UI | React components and hooks, with adapters for Next.js and other frameworks | Native UI for React, Vue, Svelte, Angular, Preact and vanilla JS |
| Headless core | Framework hooks tied to the service | `@useupup/core`, framework-agnostic |
| Server-side rules | File router: per-route types, sizes, middleware, completion callback | `createUpupHandler`: `maxFileSize`, `allowedTypes`, `getUserId`, lifecycle hooks |
| Cloud-drive sources | Not built in | Google Drive, OneDrive, Dropbox, Box |
| Camera / screen capture | Not built in | Both |
| Image editor | Not built in | React and Preact only |
| Resumable uploads | Yes (pause and resume, since v7) | Optional: S3 multipart or tus |
| i18n | Not built in | ICU locale bundles |
## Migrating from UploadThing to S3 with upup
upup is not a drop-in replacement: the API is different, and nothing here reads
an UploadThing file router. The concepts map one to one, though.
| In UploadThing | In upup |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| An API token in your environment | Your bucket credentials plus `UPUP_UPLOAD_TOKEN_SECRET` (min 16 characters) |
| A file route's allowed types and max size | `allowedTypes` and `maxFileSize` (bytes) on the server, mirrored by the `allowedFileTypes` and `maxFileSize` props |
| `.middleware()` for auth | `getUserId` (or `auth`), plus `hooks.onBeforeUpload` as an admission gate |
| `.onUploadComplete()` | The client `onFileUploadComplete` / `onUploadComplete` props, and `hooks.onFileUploaded` on multipart completes |
| `UploadButton` / `UploadDropzone` | `UpupUploader` |
The steps below use the Next.js App Router, the most common UploadThing setup.
[Express, Fastify and Hono](/docs/guides/server-mode-setup/) take the same
config object through a one-line adapter.
### 1. Create the bucket
Create the bucket, credentials and CORS rule with your provider's guide:
[AWS S3](/docs/guides/storage/aws-s3/),
[Cloudflare R2](/docs/guides/storage/cloudflare-r2/) or
[MinIO](/docs/guides/storage/minio/). Other S3-compatible services are covered
in [Storage Providers](/docs/guides/storage-providers/).
### 2. Install
```sh
npm i @useupup/next
```
`@useupup/next` bundles the React UI and the route handlers, and depends on
`@useupup/server`.
### 3. Replace the file router with one config
```ts
// lib/upup-config.ts
import { defineUpupConfig } from '@useupup/next/server'
export const upupConfig = defineUpupConfig({
storage: {
type: 'aws', // or 'r2', 'minio', … plus `endpoint` for non-AWS
bucket: process.env.S3_BUCKET!,
region: process.env.S3_REGION!,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
// Required, server-only: stable, high-entropy, min 16 chars, the same on
// every instance. createUpupHandler throws at startup without it.
uploadTokenSecret: process.env.UPUP_UPLOAD_TOKEN_SECRET!,
// Your auth, the job UploadThing's .middleware() did. Returning null
// rejects the upload.
getUserId: async req => {
const session = await getSessionFromCookie(req) // your session helper
return session?.userId ?? null
},
maxFileSize: 4 * 1024 * 1024, // 4 MB
allowedTypes: ['image/*'],
})
```
Objects are keyed `//` by default, so every
upload is namespaced to the user who made it. Override that with `keyStrategy`.
### 4. Mount the route
```ts
// app/api/upup/[...route]/route.ts
import { createUpupNextHandler } from '@useupup/next/server'
import { upupConfig } from '@/lib/upup-config'
export const { GET, POST, PUT, DELETE } = createUpupNextHandler(upupConfig)
```
The segment must be a catch-all, because the handler dispatches on the path
below it (`/presign`, `/multipart/init`, …). You can delete the old
`app/api/uploadthing` route once nothing calls it. Details:
[Next.js adapter](/docs/guides/server-adapters/nextjs/).
### 5. Swap the component
```tsx
'use client'
import { UpupUploader } from '@useupup/next'
import '@useupup/next/styles'
export default function AvatarUploader() {
return (
{
// What used to live in .onUploadComplete(): record the upload.
void fetch('/api/files', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, name: file.name }),
})
}}
/>
)
}
```
### 6. Move the completion logic
This is the one real behavioural difference. UploadThing calls your
`onUploadComplete` on the server. With upup, a single presigned `PUT` goes from
the browser straight to the bucket, so **your server never sees it finish** and
no server hook fires for it. You have two options:
- **Record it from the client**, as above. Treat the reported key as untrusted:
in `/api/files`, check that it starts with the signed-in user's id (the
default key layout makes this a prefix check), and optionally confirm the
object exists before writing the row.
- **Use the server hooks.** `hooks.onFileUploaded` fires once per file on
`POST /multipart/complete` and on cloud-drive transfers, the paths where the
server itself completes the upload. See the per-route table in
[Server Mode Setup](/docs/guides/server-mode-setup/).
### 7. Copy existing files
upup does not import files from UploadThing. Copy them with a one-off script:
list them through UploadThing's server SDK (see their docs), download each
file, upload it to your bucket, then rewrite the URLs you stored. Do this before
you cancel the plan.
## When UploadThing is the better choice
Be honest about the trade. UploadThing is the better fit when:
- **You don't want any storage account at all.** No bucket, no IAM, no CORS, no
lifecycle rules, and no cloud bill to read.
- **You're on Next.js and want the typed route definition.** The file router
keeps types, limits and auth for each upload endpoint in one typed place,
which is pleasant to work with.
- **Your volume is small and predictable**, and a plan price is simpler than
metering storage, requests and transfer separately.
- **Nobody on the team wants to own the upload infrastructure.** With a service,
outages and scaling are someone else's job.
Pick S3 plus upup when you want the files in your own account, want to choose
your storage provider (and its egress pricing), need cloud-drive, camera or
screen-capture sources, or build in Vue, Svelte, Angular or plain JS as well as
React.
## FAQ
### Is UploadThing just S3?
No. UploadThing is a service: storage, request signing, per-route upload rules
and file URLs, all run for you. An S3 bucket is only the storage. Everything
else is yours to build, or to take from a library such as upup.
### Is UploadThing free?
It has a free tier with 2 GB of storage. See
[UploadThing pricing and self-hosted costs](/docs/comparisons/uploadthing-pricing/)
for the current plans and prices. upup itself costs nothing: you pay only your
storage provider, at that provider's rates.
### Is upup a drop-in replacement for UploadThing?
No. The component, the server config and the completion hooks have different
shapes. The [concept map](#migrating-from-uploadthing-to-s3-with-upup) above
lines them up, and most apps migrate by replacing one route and one component.
### Does upup work with Cloudflare R2 or MinIO instead of AWS S3?
Yes. Server mode speaks the S3 API to any S3-compatible provider: set
`storage.type` and, for anything other than AWS, `storage.endpoint`. See
[Cloudflare R2](/docs/guides/storage/cloudflare-r2/) and
[MinIO](/docs/guides/storage/minio/).
### Do uploaded bytes go through my server?
Not for normal uploads. The server signs the request and the browser sends the
bytes straight to the bucket, in both client and server mode. The exception is
cloud-drive transfers in server mode: the server pulls the file from the drive
and streams it into the bucket in bounded 5 MiB chunks.
### What does upup cost?
Nothing. It is MIT-licensed with no hosted tier and no account. You pay only
your storage provider.
### Can I keep a hosted-service feel without the hosted service?
Mostly. Server mode gives you one route, server-chosen keys, size and type
limits, per-user namespacing and lifecycle hooks. What you take on is the
bucket itself, so start from the storage guide for your provider.
---
# upup vs Uppy — Which Uploader to Choose
https://useupup.com/docs/comparisons/upup-vs-uppy/
Uppy (by Transloadit) is the most established open-source JavaScript uploader,
with a large, battle-tested plugin ecosystem and Companion, its mature server for
fetching files from remote sources. Both Uppy and upup are MIT-licensed and share
the same shape — a headless core plus framework UI, cloud-drive sources, camera,
image editing, and resumable uploads. The practical differences are in the UI
model and the server: upup ships a **native, DOM-identical UI implemented in each
framework's own idioms** across six frameworks (including Svelte 5, Angular
standalone, a framework-free build, and Preact), and its `@useupup/server` includes
an HMAC-signed upload-token trust model out of the box. Uppy mounts one UI through
framework wrappers and has the deeper, more widely deployed plugin catalog.
## At a glance
| Feature | Uppy | upup |
| ----------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Native first-party UI | Vanilla JS, React, Vue, Svelte, Angular | React, Vue, Svelte, Angular, Vanilla JS, Preact |
| Headless core | Yes (`@uppy/core`; recent major versions add headless components and hooks) | Yes (`@useupup/core`) |
| License / pricing | MIT, free & open source (Transloadit is an optional paid hosted service) | MIT, free & open source |
| Self-host incl. S3-compatible | Yes — your endpoint / tus server / S3 via the AWS S3 plugin; Companion (self-hostable) for remote sources | Yes — `@useupup/server` signs direct uploads to any S3-compatible storage (AWS, MinIO, R2, Spaces, Wasabi, Backblaze) |
| Cloud-drive sources | Google Drive, Dropbox, OneDrive, Box, Google Photos, Unsplash, and more (via Companion) | Google Drive, OneDrive, Dropbox, Box |
| Camera / screen capture | Yes (Webcam, Screen Capture plugins) | Yes (both) |
| Image editor | Yes (Image Editor plugin) | Yes — React/Preact only |
| Resumable uploads | Yes (tus, S3 multipart) | Yes — optional (tus or S3 multipart) |
| i18n | Yes (locale packs) | Yes (ICU locale bundles) |
## Choose Uppy if
- You want the most mature, most widely deployed option, with the largest and
most battle-tested plugin ecosystem.
- You need Companion's server-side remote fetching for many providers (including
Google Photos and Unsplash) or value its long production track record.
- Your team has already standardized on Uppy's plugin model.
## Choose upup if
- You want native UI implemented in each framework's idioms — including Svelte 5,
Angular standalone, a framework-free build, and Preact — held DOM-identical by a
parity harness, rather than one UI mounted through wrappers.
- You want a server with an HMAC-signed upload-token trust model built in, so the
client can never assert the object key or S3 `uploadId` it writes to.
- You want cloud drives, camera, screen capture, image compression/HEIC, and
resumable uploads from one MIT package set with a single DOM/parity contract.
---
# Credentials and CORS
https://useupup.com/docs/credentials-configuration/
Client mode should only expose browser-safe values such as OAuth client IDs or
public Google Drive API keys. Storage credentials and OAuth client secrets
belong in server mode.
## CORS
For browser direct uploads, configure your bucket to allow the exact app
origins that will upload files.
```tsx
```
`dangerouslyAutoConfigure` can mutate storage CORS policy. Keep it for local
setup and controlled admin tooling, not broad production defaults.
## Server Credentials
Use `@useupup/server` for storage access keys, OAuth client secrets, provider
token refresh, audit logging, and server-side transfers.
---
# Error Handling
https://useupup.com/docs/error-handling/
upup reports failures as a typed `UpupError` (or one of its subclasses). Every
`UpupError` carries:
- `message` — human-readable description
- `code` — a stable string from the `UpupErrorCode` enum
- `retryable` — whether the failure is transient and worth retrying
- `status` — the originating HTTP status, when the error came from a response
## Error types
Import the error classes and the code enum from `@useupup/core`:
```typescript
import {
UpupError,
UpupErrorCode,
UpupAuthError,
UpupNetworkError,
UpupValidationError,
UpupQuotaError,
UpupStorageError,
UpupConfigError,
} from '@useupup/core'
```
| Class | Raised when | Extra fields |
| --------------------- | ----------------------------------------------------------- | ------------------------------ |
| `UpupError` | Base class for every upup error | `code`, `retryable`, `status?` |
| `UpupValidationError` | A file fails a size / type / count check | `reason`, `file` |
| `UpupNetworkError` | A fetch / XHR fails (marked `retryable`) | `status?` |
| `UpupStorageError` | An S3 / storage operation fails | `provider`, `operation` |
| `UpupAuthError` | A cloud-drive OAuth / provider call fails | `provider` |
| `UpupQuotaError` | A configured quota is exceeded | `limit`, `used` |
| `UpupConfigError` | Configuration is missing or invalid (e.g. no upload target) | — |
The `code` values come from the `UpupErrorCode` enum. See [Error
Codes](/docs/api-reference/error-codes/) for the complete list with the trigger
behind each one, or [Error Monitoring](/docs/guides/error-monitoring/) for the
codes you'll tag most often.
## Inspecting an error
Upload failures surface through the core `upload-error` event with the full
`Error` object and the file that failed. Narrow with `instanceof`, or switch on
`code`:
```typescript
import { UpupError, UpupErrorCode, UpupValidationError } from '@useupup/core'
// error is the Error object from an upload-error event (see Error Monitoring).
function describeUploadError(error: unknown): string {
if (error instanceof UpupValidationError) {
return `${error.file.name} rejected: ${error.reason}`
}
if (error instanceof UpupError) {
if (error.code === UpupErrorCode.PRESIGN_FAILED) {
return 'Your token endpoint returned an error.'
}
return error.retryable
? 'Transient failure — safe to retry.'
: 'Permanent failure — needs attention.'
}
return 'Unknown error.'
}
```
Both surfaces — the headless core's `upload-error` event (full `Error`) and the
React `onError` prop (message string) — plus wiring them into an error tracker
are covered in [Error Monitoring](/docs/guides/error-monitoring/).
## Retry behavior
Configure automatic retries with the [`maxRetries`](/docs/api-reference/upupuploader/optional-props/#maxretries) prop. Each file upload is retried up to that many times before it counts as a failure.
```tsx
```
Once a file has exhausted its attempts and the run is marked failed, a manual **"Retry"** button appears in the UI. It appears whether or not `maxRetries` is set — that prop controls how many automatic attempts happen before the failure, not whether the manual button is offered.
### Resumable upload recovery
When [resumable multipart uploads](/docs/resumable-uploads/) are enabled (`resumable={{ protocol: 'multipart' }}`), the failure UI changes and so does what recovery costs:
- The button shown **after a failure** is labelled **"Resume"** instead of "Retry".
- It invokes the same retry command as a non-resumable upload, but the file continues from its last completed part rather than restarting. The failed attempt left its server-side parts in place on purpose, and the retry re-attaches to them through `POST /multipart/resume`.
Note that this is not the **"Resume"** button shown while a run is _paused_ —
that one is a separate control on the pause/resume path. Only the failure-state
button is described here.
Part-level continuation survives a page refresh too, provided the file comes
back: pair `resumable` with `crashRecovery` and the restored file re-attaches
mid-transfer. It is on by default (`persist: true`) and degrades to a fresh
upload — never to a failure — whenever the session cannot be trusted. What
that costs your bucket, and the `AbortIncompleteMultipartUpload` lifecycle rule
it obliges you to configure, are covered in [Cross-reload
resume](/docs/resumable-uploads/#cross-reload-resume).
In client mode (`uploadEndpoint`), where multipart cannot run, tus remains the
resumable option: it delegates fingerprinting and offset recovery to
`tus-js-client`. See [Resumable Uploads](/docs/resumable-uploads/).
---
# FAQ
https://useupup.com/docs/faq/
The questions we get most often, in one place. `https://useupup.com/faq` and
`faq.useupup.com` both land here.
## Which frameworks does upup support?
upup ships one uploader with native UI packages for React, Vue, Svelte, Angular,
Vanilla JS, and Preact. Every framework renders the same DOM structure, classes,
and accessibility tree, verified by a cross-framework parity test suite, so the
uploader looks and behaves the same everywhere. Next.js gets its own package,
[`@useupup/next`](/docs/quickstarts/next/), which bundles the React UI with server
route handlers.
## Is upup free and open source?
Yes. upup is MIT-licensed and free to use in commercial and personal projects.
The source lives on GitHub at
[github.com/DevinoSolutions/upup](https://github.com/DevinoSolutions/upup). There
is no hosted service, no account, and no paid tier — uploads go to storage you
own.
## Does upup work with S3-compatible storage like MinIO or Cloudflare R2?
Yes. upup uploads to any S3-compatible storage, including AWS S3, Cloudflare R2,
MinIO, DigitalOcean Spaces, Backblaze B2, Wasabi, and more, as well as Azure Blob
Storage. Provider-specific setup, including the CORS rules each bucket needs, is
under [Guides → Storage](/docs/guides/storage/aws-s3/).
## Which cloud drives can users upload from?
Users can import files directly from Google Drive, OneDrive, Dropbox, and Box.
upup also supports uploads from the device camera, screen capture, and link (URL)
imports. Each drive needs an OAuth client from that provider's developer console;
see [Credentials configuration](/docs/credentials-configuration/).
## What is the difference between client mode and server mode?
In both modes the browser sends local file bytes straight to your bucket over
short-lived presigned URLs, and your storage credentials stay on your server.
In client mode you write the endpoint that signs those URLs, and cloud-drive
sign-in and downloads run in the browser. In server mode the `@useupup/server`
package is that endpoint — HMAC-signed upload tokens, per-user auth, size and
type limits, and multipart coordination — and it also runs cloud-drive OAuth
and streams drive files into your bucket from the server. Start with
[Server Mode — Setup](/docs/guides/server-mode-setup/) when you need server mode.
## Can I use my own UI with upup?
Yes. The engine lives in `@useupup/core`, a framework-agnostic headless package.
You can use the built-in native UI for your framework or build a fully custom
interface on the same core using the exported hooks and controllers. See
[Headless core](/docs/guides/headless/).
## Why does `createUpupHandler` throw at startup?
The server handler requires an `uploadTokenSecret` of at least 16 characters and
throws at construction time if it is missing or too short — it is the key that
HMAC-signs every upload token, so the handler refuses to start without one. Set
it from an environment variable and keep it identical on every server instance.
Details in [Server Mode — Setup](/docs/guides/server-mode-setup/).
## Why do uploads fail with a CORS error?
The browser PUTs file bytes straight to your bucket in both client and server
mode, so the bucket itself must allow your origin, the `PUT` method, and the
headers the presigned URL carries. The per-provider pages under
[Guides → Storage](/docs/guides/storage/aws-s3/) include the exact CORS rule to
apply. The presign endpoint returning a URL but the upload failing afterwards is
almost always this.
## Which React and Next.js versions are required?
`@useupup/react` and `@useupup/preact` require React 19. `@useupup/next` requires
Next.js 15 or newer. If you are on Next.js, add
`serverExternalPackages: ['@aws-sdk/client-s3', '@aws-sdk/s3-request-presigner']`
to `next.config` so the AWS SDK stays out of your client bundle — see the
[Next.js quickstart](/docs/quickstarts/next/).
## Why is the image editor missing in Vue, Svelte, Angular, or Vanilla JS?
The image editor (crop, rotate, annotate) ships for React and Preact only; the
other frameworks intentionally stub it. Every other feature — cloud drives,
camera, screen capture, link import, compression, HEIC conversion, resumable
uploads, i18n, and theming — is identical across all six frameworks.
## How do I set up an AI coding agent to use upup?
Copy the "Onboard your agent to upup" prompt from
[/agent-setup/](/agent-setup/) and paste it into Claude Code, Codex, Cursor, or
OpenCode. The agent fetches `/agent-setup/prompt.md`, installs the right
`@useupup/*` package, mounts the uploader, and saves a verified context block into
its instructions file. The context block on its own is on
[Use upup with AI assistants](/docs/ai-assistants/).
## Where do I ask something that is not answered here?
Open a [GitHub issue](https://github.com/DevinoSolutions/upup/issues) for bugs and
feature requests, ask the community on
[our Discord](https://discord.gg/ny5WUE9ayc), or use the
[support form](/support/) to reach the team directly.
---
# Getting Started
https://useupup.com/docs/getting-started/
upup is a file uploader with a native UI for **React, Vue, Svelte, Angular,
Vanilla JS, and Preact**, built on a shared headless core (`@useupup/core`), with
an optional server mode for signed uploads and cloud-drive sources (Google
Drive, OneDrive, Dropbox, Box). Every package renders the same UI.
Here is the uploader you are about to install — try it:
This guide uses React. To start from another framework, use its quickstart —
each mounts the same uploader with the same options:
- [React quickstart](/docs/quickstarts/react/)
- [Vue quickstart](/docs/quickstarts/vue/)
- [Svelte quickstart](/docs/quickstarts/svelte/)
- [Angular quickstart](/docs/quickstarts/angular/)
- [Vanilla JS quickstart](/docs/quickstarts/vanilla/)
- [Preact quickstart](/docs/quickstarts/preact/)
Install the React package and styles:
```bash
npm i @useupup/react
```
```tsx
import { UpupUploader } from '@useupup/react'
import '@useupup/react/styles'
```
## Pick your framework
The same uploader mounts from every package with the same options. Pick your
framework — the choice is remembered as you move through the docs, and you can
deep-link a framework with `?fw=vue`.
## Local file collection
With no upload target, upup lets users select files and gives you `File`
objects through callbacks and hooks.
```tsx
{
console.log(files)
}}
/>
```
Calling `upload()` without a target returns a typed no-target error.
## What happens to your file
Whichever target you pick, every file runs through the same client-side pipeline
before it leaves the browser — validated, optionally compressed (HEIC images are
converted), then uploaded, with the heavy work offloaded to a web worker.
## Client uploads
Use `uploadEndpoint` when your app signs upload URLs and the browser uploads
bytes directly to storage.
```tsx
```
## Server uploads
Use `@useupup/server` when provider OAuth, token storage, storage credentials,
or transfer policy should live server-side.
```bash
npm i @useupup/react @useupup/server
```
```tsx
```
```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!,
tokenStore: new InMemoryTokenStore(),
getUserId: async () => 'user_123',
})
export const GET = handler
export const POST = handler
```
---
# Accessibility
https://useupup.com/docs/guides/accessibility/
upup's accessibility semantics are not an opt-in layer. Roles, labels, live
regions, focus handling, and the reduced-motion gate are part of the default
render — there is no `a11y` prop to switch on, and nothing here requires extra
markup from you.
Because every framework package renders the same DOM (React is the source of
truth and the cross-framework parity harness compares normalized DOM plus an
accessibility snapshot for React, Vue, Svelte, Angular, Vanilla, and Preact),
the behavior described on this page is identical in all six. A nightly CI job
(`pnpm run e2e:a11y`) runs axe-core against every framework and fails the build
on any new serious or critical violation.
## Keyboard interaction
Every actionable element in the uploader is a real `