Documentation menu

 

UploadThing vs S3: A Self-Hosted Alternative

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? 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.

ConcernUploadThingYour own S3 bucket
Where the files liveStorage that UploadThing operatesA bucket in your own cloud account (or your own MinIO)
Who you payUploadThing, per its plans (see their pricing page)Your storage provider, at its storage, request and transfer rates
Egress (downloads)Governed by UploadThing's plan termsBilled by your provider; some S3-compatible providers do not charge for it
File URLs you storePoint at UploadThing's domainsPoint at your bucket, your CDN or your own domain
Region and residencyChosen by the serviceYou pick the region, or run storage on-premises
Switching away laterExport the files and rewrite every stored URLThe S3 API is implemented by many providers, so a move is mostly an endpoint change
Setup workMinimal: an API key and a file routerBucket, 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.

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 PUTs 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.
  • 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.

2. CORS on the bucket

Whenever the browser PUTs 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, Cloudflare R2, 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.

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.

Feature comparison

FeatureUploadThingupup
ModelHosted service (managed storage and file URLs)Self-hosted library (your storage)
License / pricingMIT SDK; hosted service with a free 2 GB tier and paid plansMIT, free; you pay only your storage provider
StorageUploadThing's infrastructureAny S3-compatible bucket: AWS, R2, MinIO, Spaces, Wasabi, Backblaze B2 and more
First-party UIReact components and hooks, with adapters for Next.js and other frameworksNative UI for React, Vue, Svelte, Angular, Preact and vanilla JS
Headless coreFramework hooks tied to the service@useupup/core, framework-agnostic
Server-side rulesFile router: per-route types, sizes, middleware, completion callbackcreateUpupHandler: maxFileSize, allowedTypes, getUserId, lifecycle hooks
Cloud-drive sourcesNot built inGoogle Drive, OneDrive, Dropbox, Box
Camera / screen captureNot built inBoth
Image editorNot built inReact and Preact only
Resumable uploadsYes (pause and resume, since v7)Optional: S3 multipart or tus
i18nNot built inICU 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 UploadThingIn upup
An API token in your environmentYour bucket credentials plus UPUP_UPLOAD_TOKEN_SECRET (min 16 characters)
A file route's allowed types and max sizeallowedTypes and maxFileSize (bytes) on the server, mirrored by the allowedFileTypes and maxFileSize props
.middleware() for authgetUserId (or auth), plus hooks.onBeforeUpload as an admission gate
.onUploadComplete()The client onFileUploadComplete / onUploadComplete props, and hooks.onFileUploaded on multipart completes
UploadButton / UploadDropzoneUpupUploader

The steps below use the Next.js App Router, the most common UploadThing setup. Express, Fastify and Hono 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, Cloudflare R2 or MinIO. Other S3-compatible services are covered in 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 <userId>/<uuid>/<sanitized-filename> 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.

5. Swap the component

tsx
'use client'

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

export default function AvatarUploader() {
    return (
        <UpupUploader
            mode="server"
            serverUrl="/api/upup"
            provider="aws"
            allowedFileTypes="image/*"
            maxFileSize={{ size: 4, unit: 'MB' }}
            onFileUploadComplete={(file, key) => {
                // 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.

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 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 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 and 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.