Documentation menu

 

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.

Client modeYour endpointsigns URLsBrowserpresignPUT file bytes (presigned URL)S3-compatiblestorageServer modeYour server@useupup/serverCloud driveGoogle, OneDrive, …OAuth + pullBrowserpresignPUT local file bytes (presigned URL)S3-compatiblestoragedrive files stream server → storage and never pass through the browser
tsx
<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_REQUIRED until you configure auth, getUserId, or allowAnonymousUploads.
  • You want server-side admission: hooks.onBeforeUpload(file, req) can reject an upload, and maxFileSize / allowedTypes are enforced, before any URL is issued.
  • You want the server to choose object keys — <userId>/<uuid>/<filename> by default, so with getUserId set 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?

ConcernClient modeServer mode
Upload targetuploadEndpointserverUrl
Signing endpointYou write it, any backendcreateUpupHandler (@useupup/server)
Local file bytesBrowser → bucket (presigned PUT)Browser → bucket (presigned PUT)
Drive file bytesProvider → browser → bucketProvider → server → bucket
OAuth client IDShipped to browserServer config only
OAuth client secretNot usedServer-only, required
Drive API callsBrowser → providerServer → provider
Storage credentialsYour server; browser gets URLsYour server; browser gets URLs
MultipartNot available (single PUT)Server-signed parts, browser PUTs
Drive token expiryBrowser refreshes; Google re-authServer 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.