Client Mode vs Server Mode
@useupup/react supports local-only collection plus two upload hosts. mode
controls where upload/provider operations run, not whether users can select
local files. In both modes local file bytes go straight from the browser to
your bucket over short-lived presigned URLs, and your storage credentials stay
on your server.
<UpupUploader /> // local file collection only
<UpupUploader uploadEndpoint="/api/upload-token" /> // client-hosted upload flow
<UpupUploader mode="server" serverUrl="/api/upup" />Client mode
You write the signing endpoint. Your server's only job is to sign short-lived upload URLs; the browser uploads straight to storage.
browser ──POST /sign──> your server
browser <──presigned URL─ your server
browser ──PUT bytes────> S3 / R2 / etc.Cloud drives (Google Drive, OneDrive, Dropbox, Box) use OAuth from the browser — tokens stay in browser memory, never touch your server. Picked drive files download into the browser, then upload like local files.
Choose client mode when:
- You want the simplest setup.
- You're OK exposing OAuth client IDs (not secrets) to the browser.
- Your backend isn't JavaScript, or you already sign S3 URLs — the endpoint is one small route in any language.
- Each file fits a single PUT — S3 multipart and cross-reload resume need server mode.
Server mode
@useupup/server is your signing endpoint and your drive backend. Local
file bytes still go straight from the browser to storage; the server decides
who may upload what, and where it lands. Cloud-drive OAuth, listing, and
transfers run on the server, so drive bytes never touch the browser.
local files — signed on your server, bytes go direct:
browser ──POST /presign, /multipart/*──> your server
browser <──presigned URL(s)───────────── your server
browser ──PUT bytes──────────────────> S3 / R2 / etc.
cloud drives — OAuth, listing, and transfer on your server:
browser ──GET /files/:provider ──> your server ──> Google / MS / ...
browser ──POST /files/:provider/transfer ──> your server ──> S3
(drive → server → S3, in bounded 5 MiB chunks)The uploader calls one API origin, plus your bucket for the PUTs — so the
bucket needs the same CORS rule as in client mode. You own the drive access
tokens and the storage credentials; neither reaches the browser. Sending local
bytes through a server is opt-in: hooks.onPresignResponse can rewrite the
upload URL to a same-origin proxy route that you run (see
Rewriting presign responses).
Choose server mode when:
- You want a ready-made signing endpoint that is secure by default — upload
routes return
403 AUTH_REQUIREDuntil you configureauth,getUserId, orallowAnonymousUploads. - You want server-side admission:
hooks.onBeforeUpload(file, req)can reject an upload, andmaxFileSize/allowedTypesare enforced, before any URL is issued. - You want the server to choose object keys —
<userId>/<uuid>/<filename>by default, so withgetUserIdset each user writes into their own namespace. - Cloud-drive OAuth must stay off the browser (the client secret and access tokens live on your server), or your users can't reach the drive APIs directly (corporate firewall, region block).
- Drive files may be large. The server pulls the picked file and streams it into the bucket in bounded 5 MiB chunks, so it never downloads to the browser.
- You need S3 multipart uploads that resume after a reload.
Server-side completion hooks only see what the server finishes itself:
onFileUploaded fires on multipart complete and drive transfers,
onUploadComplete on multipart complete only. A single presigned PUT never
reaches your server on completion — see
Lifecycle hooks.
What changes between modes?
| Concern | Client mode | Server mode |
|---|---|---|
| Upload target | uploadEndpoint | serverUrl |
| Signing endpoint | You write it, any backend | createUpupHandler (@useupup/server) |
| Local file bytes | Browser → bucket (presigned PUT) | Browser → bucket (presigned PUT) |
| Drive file bytes | Provider → browser → bucket | Provider → server → bucket |
| OAuth client ID | Shipped to browser | Server config only |
| OAuth client secret | Not used | Server-only, required |
| Drive API calls | Browser → provider | Server → provider |
| Storage credentials | Your server; browser gets URLs | Your server; browser gets URLs |
| Multipart | Not available (single PUT) | Server-signed parts, browser PUTs |
| Drive token expiry | Browser refreshes; Google re-auth | Server refreshes if it holds a refresh token |
Feature parity
Every UpupUploader event callback (onFilesSelected, onUploadStart,
onFileUploadComplete, etc.) fires identically in both modes. Theme
slots, i18n, file limits (maxFiles, maxFileSize, allowedFileTypes),
image editor — all work the same.
The differences are strictly on the wire, not on the API surface.
Migration
If neither uploadEndpoint nor serverUrl is configured, the uploader stays
in local file collection mode and calling upload() returns a typed no-target
error.