Comparison
How upload-stuff compares to UploadThing and a hand-rolled presigned-S3 flow.
upload-stuff sits between a managed upload service and rolling your own presigned flow.
Bring your own bucket
UploadThing manages storage for you — no S3 account, no bucket, no CORS. upload-stuff expects your own S3 bucket and credentials: you own your data, your bucket policy, and your bill, and you get an end-to-end typed API over it.
If you never want to touch S3, a managed service is the simpler choice. If you already have (or want) your own bucket, upload-stuff removes the boilerplate without taking over your storage.
The same upload, three ways
avatar: f({ isPublic: true, files: ["image/*"], maxFileSize: "4MB" })
.middleware(({ ctx }) => ({ userId: ctx.userId }))
.onUploadComplete(({ middlewareData }) => ({ uploadedBy: middlewareData.userId }))
.build();One typed route — middlewareData and the completion payload are fully typed,
and the client hook infers its types from this definition
(proven, not asserted).
avatar: f({ image: { maxFileSize: "4MB" } })
.middleware(async ({ req }) => ({ userId: await auth(req) }))
.onUploadComplete(({ metadata, file }) => {
console.log(file.url);
});file.url points at UploadThing's CDN — managed storage means no bucket to
configure, and no bucket you own.
const command = new PutObjectCommand({ Bucket, Key, ContentType });
const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
await fetch(url, { method: "PUT", body: file });And that is only the presign step — the verification, the database row, the cleanup of abandoned uploads, and the typed client wiring are still yours to write and keep correct.
What you stop owning
- The presigned-URL handshake and its expiry window.
- Storage verification when the client claims completion.
- Pending-row bookkeeping in your database.
- Finding and deleting abandoned uploads — you schedule
cleanUpFiles, it does the sweep. - The typed client wiring.
See Type safety for the router-to-client inference end-to-end.