# UploadCN, full documentation # HTTP (/docs/adapters/http) ```ts import { httpAdapter } from "@uploadcn/core" const adapter = httpAdapter<{ id: string; url: string }>({ url: "/api/files", fieldName: "file", fields: (item) => ({ folder: String(item.meta.folder ?? "inbox") }), headers: async () => ({ authorization: `Bearer ${await getToken()}` }), }) ``` Sends `multipart/form-data` by default; `body: "binary"` sends the raw file with its MIME type (useful for `PUT` endpoints). The response is parsed as JSON when possible. ## Example: Next.js route handler [#example-nextjs-route-handler] ```ts title="app/api/files/route.ts" export async function POST(request: Request) { const form = await request.formData() const file = form.get("file") as File const url = await saveSomewhere(file) return Response.json({ url }) } ``` Proxying files through your server is simple but costs bandwidth and is limited by platform body-size limits (4.5 MB on Vercel functions). Prefer [presigned uploads](/docs/guides/presigned-uploads) for large files. --- # Adapters (/docs/adapters) An adapter is a plain object with an `upload` function. The engine handles queueing, retries, pause/cancel, progress throttling, speed and persistence; the adapter only moves bytes and reports progress. ```ts interface UploadAdapter { name: string /** Pausing is offered only for resumable uploads. */ resumable?: boolean | ((file: File) => boolean) upload(context: UploadAdapterContext): Promise /** Clean up server state (e.g. abort a multipart upload) on cancel. */ abort?(context: { item; resumeState }): Promise } ``` ## Built-in adapters [#built-in-adapters] | Adapter | For | | ---------------------------------------------- | ------------------------------------------------------------- | | [`s3Adapter`](/docs/adapters/s3) | S3 and S3-compatible storage, single PUT or multipart | | [`r2Adapter`](/docs/adapters/r2) | Cloudflare R2 (alias of `s3Adapter`) | | [`presignedAdapter`](/docs/adapters/presigned) | Any service that issues signed URLs or POST policies | | [`multipartAdapter`](/docs/adapters/multipart) | Any chunked protocol: bring create / uploadPart / complete | | [`httpAdapter`](/docs/adapters/http) | A plain endpoint that accepts files | | [`tusAdapter`](/docs/adapters/tus) | tus servers (tusd, @tus/server, Supabase, Cloudflare Stream…) | | [`mockAdapter`](/docs/adapters/mock) | Prototypes, tests and docs | | `routeAdapter` | Pick an adapter per file | ```ts import { MiB, httpAdapter, routeAdapter } from "@uploadcn/core" import { tusAdapter } from "@uploadcn/core/tus" const adapter = routeAdapter((file) => file.size > 100 * MiB ? tusAdapter({ endpoint: "/files" }) : httpAdapter({ url: "/upload" }) ) ``` ## Writing your own [#writing-your-own] The context gives you everything you need: ```ts interface UploadAdapterContext { item: UploadItem file: File // after transforms signal: AbortSignal // aborted on pause, cancel and remove attempt: number // 1-based, within the current run resumeState: unknown // what you saved last time onProgress(loaded: number, total?: number): void onChunkProgress(completed: number, total: number): void saveResumeState(state: unknown): void } ``` Throw an [`UploadError`](/docs/reference/core#uploaderror) with `retryable: true` for failures worth retrying. ### Supabase Storage [#supabase-storage] ```ts import { presignedAdapter } from "@uploadcn/core" export const supabaseAdapter = presignedAdapter({ async getTarget({ file }) { // Your route calls supabase.storage.from(bucket).createSignedUploadUrl(path) const { signedUrl, path } = await fetch("/api/supabase-upload", { method: "POST", body: JSON.stringify({ name: file.name }), }).then((res) => res.json()) return { url: signedUrl, key: path } }, }) ``` ### Firebase Storage [#firebase-storage] ```ts import { UploadError, type UploadAdapter } from "@uploadcn/core" import { getStorage, ref, uploadBytesResumable, getDownloadURL } from "firebase/storage" export const firebaseAdapter: UploadAdapter<{ path: string; url: string }> = { name: "firebase", upload: ({ file, signal, onProgress }) => new Promise((resolve, reject) => { const path = `uploads/${crypto.randomUUID()}-${file.name}` const task = uploadBytesResumable(ref(getStorage(), path), file) signal.addEventListener("abort", () => task.cancel()) task.on( "state_changed", (snapshot) => onProgress(snapshot.bytesTransferred, snapshot.totalBytes), (error) => reject(new UploadError(error.message, { code: "network", retryable: true })), async () => resolve({ path, url: await getDownloadURL(task.snapshot.ref) }) ) }), } ``` ### Azure Blob Storage and Google Cloud Storage [#azure-blob-storage-and-google-cloud-storage] Both issue signed URLs from your server (SAS tokens / V4 signed URLs), so they work with [`presignedAdapter`](/docs/adapters/presigned). For very large files, use [`multipartAdapter`](/docs/adapters/multipart) with Azure's Put Block / Put Block List or GCS's XML multipart API. ### UploadThing [#uploadthing] UploadThing ships its own React client. If you want UploadCN's UI on top of it, wrap its `uploadFiles` helper in an adapter's `upload` function and forward `onUploadProgress` to `onProgress`. --- # Mock (/docs/adapters/mock) `mockAdapter` simulates throughput, latency, chunks, failures and resume, so you can build and test every UI state before your backend exists. ```ts import { MiB, UploadError, mockAdapter } from "@uploadcn/core" const adapter = mockAdapter({ speed: 2 * MiB, // bytes per second latency: 250, // ms before the first byte chunkSize: 5 * MiB, // report chunk progress resumable: true, // pause continues from the last byte failureRate: 0.1, // 10% of attempts fail midway (retryable) fail: ({ file, attempt }) => file.name.includes("forbidden") ? new UploadError("Forbidden", { code: "http", status: 403 }) : null, }) ``` Results look like `{ key, url: "mock://uploads/…" }`. ## In tests [#in-tests] The engine is plain TypeScript, so you can drive it from Vitest without a DOM: ```ts const uploader = createUploader({ adapter: mockAdapter({ latency: 0, speed: 1e9 }), network: false }) const [item] = await uploader.add([new File(["hello"], "a.txt")]) await vi.waitFor(() => expect(uploader.getItem(item.id)?.status).toBe("success")) ``` --- # Multipart & chunked (/docs/adapters/multipart) `multipartAdapter` splits files into parts, uploads them in parallel, retries failed parts independently, reports aggregate and chunk progress, and records finished parts so uploads resume after a pause, a network loss, or a page reload. You bring three functions; it works with S3's multipart API, GCS's XML API, Azure block blobs, or your own chunk endpoint. ```ts import { multipartAdapter, MiB } from "@uploadcn/core" const adapter = multipartAdapter({ partSize: 8 * MiB, concurrency: 4, retry: { retries: 3, baseDelay: 500 }, create: ({ file }) => api.createUpload(file), // → session uploadPart: ({ session, partNumber, blob, signal, onProgress }) => api.putPart(session, partNumber, blob, { signal, onProgress }), // → { etag } complete: ({ session, parts }) => api.complete(session, parts), // → result abort: ({ session }) => api.abort(session), listParts: ({ session }) => api.listParts(session), // optional }) ``` ## Options [#options] ## Resume state [#resume-state] After each finished part the adapter saves: ```ts { session, partSize, fileSize, parts: [{ partNumber, etag, size }] } ``` With [persistence](/docs/guides/resumable-uploads), this survives reloads. When the saved state doesn't match the file (different size or part size), the upload starts over cleanly. ## Retries at two levels [#retries-at-two-levels] 1. A failed **part** is retried with backoff without touching other parts. 2. If a part exhausts its retries, the **item** fails; the engine's own retry policy then retries the item, which resumes from the parts already stored. --- # Presigned URLs (/docs/adapters/presigned) `presignedAdapter` implements the generic presigned flow for any provider: Google Cloud Storage V4 signed URLs, Azure SAS URLs, Supabase signed upload URLs, S3 POST policies, or your own signing service. ```ts import { presignedAdapter } from "@uploadcn/core" const adapter = presignedAdapter({ async getTarget({ file, signal }) { const response = await fetch("/api/sign", { method: "POST", body: JSON.stringify({ name: file.name, type: file.type, size: file.size }), signal, }) return response.json() // { url, method?, headers?, fields?, key?, publicUrl? } }, async complete({ target, file }) { const response = await fetch("/api/files", { method: "POST", body: JSON.stringify({ key: target.key, name: file.name }), }) return response.json() }, }) ``` ## Target [#target] Without `complete`, the result is `{ key, url }` (the URL without its signature query). For S3 and R2 you don't need to write the signing route yourself, use [`s3Adapter`](/docs/adapters/s3) with `@uploadcn/server`. --- # Cloudflare R2 (/docs/adapters/r2) R2 implements the S3 API, so the browser code is identical to S3, `r2Adapter` is an alias of `s3Adapter`. The difference is on the server. ## Server [#server] ```ts title="app/api/upload/route.ts" import { createUploadRoute, r2Storage } from "@uploadcn/server" export const { POST } = createUploadRoute({ storage: r2Storage({ accountId: process.env.R2_ACCOUNT_ID!, bucket: process.env.R2_BUCKET!, accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, // A custom domain or r2.dev URL for reading objects: publicUrl: process.env.R2_PUBLIC_URL, }), }) ``` `r2Storage` uses `https://.r2.cloudflarestorage.com`, region `auto` and path-style URLs. For jurisdiction-restricted buckets pass `jurisdiction: "eu"`. `@uploadcn/server` signs with Web Crypto, so the same route runs on Cloudflare Workers. ## CORS [#cors] In the R2 dashboard, under **Settings → CORS policy**: ```json [ { "AllowedOrigins": ["https://your-app.com"], "AllowedMethods": ["PUT"], "AllowedHeaders": ["content-type"], "ExposeHeaders": ["ETag"] } ] ``` ## Client [#client] ```ts import { r2Adapter } from "@uploadcn/core" const adapter = r2Adapter({ endpoint: "/api/upload" }) ``` --- # Amazon S3 (/docs/adapters/s3) ## How it works [#how-it-works] 1. The browser validates the file and asks your route for a signed URL. 2. The route checks auth, size and type, and signs a `PUT` whose signature locks the **content type and content length**: S3 rejects anything else. 3. The browser uploads directly to the bucket and tracks progress. 4. The browser calls `complete`; the route verifies the object with `HEAD` and runs `onUploadComplete` (save metadata, enqueue a scan…). Files at or above `multipart.threshold` (64 MB by default) use S3 multipart instead: parts are signed one at a time, uploaded in parallel, retried individually, and the upload can pause, resume, and survive reloads. ## Client [#client] ```ts import { MiB, s3Adapter } from "@uploadcn/core" const adapter = s3Adapter({ endpoint: "/api/upload", multipart: { threshold: 64 * MiB, partSize: 8 * MiB, concurrency: 4 }, headers: async () => ({ authorization: `Bearer ${await getToken()}` }), }) ``` The result is a `StoredObject`: `{ key, url, etag?, data? }`. ## Server [#server] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/upload-route ``` ```bash pnpm dlx shadcn@latest add @uploadcn/upload-route ``` ```bash yarn dlx shadcn@latest add @uploadcn/upload-route ``` ```bash bun x shadcn@latest add @uploadcn/upload-route ``` ```ts title="app/api/upload/route.ts" import { UploadRouteError, createUploadRoute, s3Storage } from "@uploadcn/server" import { auth } from "@/lib/auth" import { db } from "@/lib/db" export const { POST } = createUploadRoute({ storage: s3Storage({ bucket: process.env.S3_BUCKET!, region: process.env.S3_REGION!, accessKeyId: process.env.S3_ACCESS_KEY_ID!, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, }), maxFileSize: 500 * 1024 * 1024, allowedTypes: ["image/*", "application/pdf"], async authorize({ key }) { const session = await auth() if (!session) throw new UploadRouteError("Unauthorized", 401) // Multipart actions carry the key: make sure it's the caller's. if (key && !key.startsWith(`${session.user.id}/`)) throw new UploadRouteError("Forbidden", 403) return session.user }, getKey: ({ auth, file }) => `${auth.id}/${crypto.randomUUID()}/${file.name}`, async onUploadComplete({ auth, key, file }) { return db.file.create({ data: { userId: auth.id, key, name: file.name, size: file.size } }) }, }) ``` See the [server reference](/docs/reference/server) for every option. ## Bucket CORS [#bucket-cors] ```json [ { "AllowedOrigins": ["https://your-app.com"], "AllowedMethods": ["PUT"], "AllowedHeaders": ["content-type"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3600 } ] ``` `ExposeHeaders: ["ETag"]` is required for multipart uploads: the browser reads each part's ETag to complete the upload. ## S3-compatible services [#s3-compatible-services] Pass `endpoint` to `s3Storage` for MinIO, Backblaze B2, DigitalOcean Spaces, Wasabi and others. Path-style URLs are used automatically with a custom endpoint. ```ts s3Storage({ endpoint: "https://s3.us-west-004.backblazeb2.com", region: "us-west-004", bucket: "uploads", accessKeyId: process.env.B2_KEY_ID!, secretAccessKey: process.env.B2_APPLICATION_KEY!, }) ``` ## Abandoned multipart uploads [#abandoned-multipart-uploads] Parts of cancelled uploads are aborted automatically. Parts of uploads that are simply abandoned (tab closed, never resumed) are billed until removed, add a bucket lifecycle rule to abort incomplete multipart uploads after a few days. --- # tus (/docs/adapters/tus) [tus](https://tus.io) is an open protocol for resumable uploads, supported by tusd, `@tus/server`, Supabase Storage, Cloudflare Stream, Vimeo and others. UploadCN wraps the reference client, `tus-js-client`, rather than reimplementing the protocol. npm pnpm yarn bun ```bash npm install tus-js-client ``` ```bash pnpm add tus-js-client ``` ```bash yarn add tus-js-client ``` ```bash bun add tus-js-client ``` ```ts import { tusAdapter } from "@uploadcn/core/tus" const adapter = tusAdapter({ endpoint: "https://tusd.example.com/files/", chunkSize: 8 * 1024 * 1024, headers: async () => ({ authorization: `Bearer ${await getToken()}` }), metadata: { bucket: "videos" }, }) ``` The engine stores the tus upload URL as resume state, so it works with [IndexedDB persistence](/docs/guides/resumable-uploads) across reloads (tus-js-client's own localStorage fingerprinting is turned off). Cancelling an upload sends a tus termination request. --- # Animated Queue (/docs/components/animated-upload-queue) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/animated-upload-queue ``` ```bash pnpm dlx shadcn@latest add @uploadcn/animated-upload-queue ``` ```bash yarn dlx shadcn@latest add @uploadcn/animated-upload-queue ``` ```bash bun x shadcn@latest add @uploadcn/animated-upload-queue ``` Install [Upload](/docs/components/upload) and `motion`, then copy: npm pnpm yarn bun ```bash npm install motion ``` ```bash pnpm add motion ``` ```bash yarn add motion ``` ```bash bun add motion ``` ## Usage [#usage] ```tsx import { AnimatedUploadQueue } from "@/components/animated-upload-queue" ``` ```tsx ``` Built from the same `UploadItem` parts as the static queue, wrapped in `AnimatePresence` for exit animations. ## Props [#props] --- # Assignment Submission (/docs/components/assignment-submission) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/assignment-submission ``` ```bash pnpm dlx shadcn@latest add @uploadcn/assignment-submission ``` ```bash yarn dlx shadcn@latest add @uploadcn/assignment-submission ``` ```bash bun x shadcn@latest add @uploadcn/assignment-submission ``` Install [Upload](/docs/components/upload) and the shadcn `badge`, `button` components, then copy: ## Usage [#usage] ```tsx import { AssignmentSubmission } from "@/components/assignment-submission" ``` ```tsx turnIn(files)} /> ``` The countdown renders on the client only, so server and client HTML always match. ## Props [#props] --- # Audio Upload (/docs/components/audio-upload) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/audio-upload ``` ```bash pnpm dlx shadcn@latest add @uploadcn/audio-upload ``` ```bash yarn dlx shadcn@latest add @uploadcn/audio-upload ``` ```bash bun x shadcn@latest add @uploadcn/audio-upload ``` Install [Upload](/docs/components/upload) first, then copy: ## Usage [#usage] ```tsx import { AudioUpload } from "@/components/audio-upload" ``` ```tsx ``` ## Props [#props] --- # Avatar Upload (/docs/components/avatar-upload) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/avatar-upload ``` ```bash pnpm dlx shadcn@latest add @uploadcn/avatar-upload ``` ```bash yarn dlx shadcn@latest add @uploadcn/avatar-upload ``` ```bash bun x shadcn@latest add @uploadcn/avatar-upload ``` Install [Upload](/docs/components/upload) first, then copy: ## Usage [#usage] ```tsx import { AvatarUpload } from "@/components/avatar-upload" ``` ```tsx updateUser({ image: item?.result?.url ?? null })} /> ``` ## Props [#props] --- # Battery (/docs/components/battery-upload) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/battery-upload ``` ```bash pnpm dlx shadcn@latest add @uploadcn/battery-upload ``` ```bash yarn dlx shadcn@latest add @uploadcn/battery-upload ``` ```bash bun x shadcn@latest add @uploadcn/battery-upload ``` Install [Upload](/docs/components/upload) and `motion`, then copy: npm pnpm yarn bun ```bash npm install motion ``` ```bash pnpm add motion ``` ```bash yarn add motion ``` ```bash bun add motion ``` ## Usage [#usage] ```tsx import { BatteryUpload } from "@/components/battery-upload" ``` ```tsx ``` Five cells share one spring, each filling its fifth of the progress. Failed uploads turn the cells destructive. The caption is an `aria-live` region with counts, bytes and time left. It uploads through your [adapter](/docs/storage) like every other component, so it works with S3, R2, Cloudinary, local disk or your own API. ## Props [#props] --- # Blueprint (/docs/components/blueprint-upload) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/blueprint-upload ``` ```bash pnpm dlx shadcn@latest add @uploadcn/blueprint-upload ``` ```bash yarn dlx shadcn@latest add @uploadcn/blueprint-upload ``` ```bash bun x shadcn@latest add @uploadcn/blueprint-upload ``` Install [Upload](/docs/components/upload) and `motion`, then copy: npm pnpm yarn bun ```bash npm install motion ``` ```bash pnpm add motion ``` ```bash yarn add motion ``` ```bash bun add motion ``` ## Usage [#usage] ```tsx import { BlueprintUpload } from "@/components/blueprint-upload" ``` ```tsx ``` Each line of the drawing takes its share of the progress, so the building is drafted in order. A dashed ghost keeps the sheet readable before anything uploads. The title block lists the latest sheet, file count and size. It uploads through your [adapter](/docs/storage) like every other component, so it works with S3, R2, Cloudinary, local disk or your own API. ## Props [#props] --- # Camera Upload (/docs/components/camera-upload) ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @uploadcn/camera-upload ``` ```bash pnpm dlx shadcn@latest add @uploadcn/camera-upload ``` ```bash yarn dlx shadcn@latest add @uploadcn/camera-upload ``` ```bash bun x shadcn@latest add @uploadcn/camera-upload ``` Install [Upload](/docs/components/upload), then copy: ## Usage [#usage] ```tsx import { CameraUpload } from "@/components/camera-upload" ``` ```tsx ``` `CameraCapture` is exported too, drop it inside any `` to add a viewfinder. The [`useCamera`](#usecamera) hook releases the camera on unmount. ## useCamera [#usecamera] The hook behind the viewfinder. It's installed with the component. ```tsx const { videoRef, status, start, stop, flip, capture } = useCamera()