---
title: "Uploadthing"
description: "Uploadthing as a MainSDK files driver, with setup, main.yaml, a direct browser upload example and the guarantees MainSDK keeps on top."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/uploadthing"
updated: "2026-10-28"
---

# Uploadthing

> Uploadthing as a MainSDK files driver, with setup, main.yaml, a direct browser upload example and the guarantees MainSDK keeps on top.

Canonical: https://mainsdk.dev/docs/integrations/uploadthing · Updated: 2026-10-28

Uploadthing is a hosted file upload service for TypeScript apps. The `uploadthing` driver for [`files`](https://mainsdk.dev/docs/interfaces/files) lets you keep Uploadthing's storage and delivery while MainSDK adds owner scope, scanning and short-lived URLs. You would pick it for the quickest path to uploads in a Next.js app without running a bucket.

> **Note:** The driver runs on Uploadthing's REST API through the network layer, with no `uploadthing` package (spec `files-share`, ADR-0013). Its conformance run uses recorded response shapes, not the live service.

## Interfaces it serves

### File uploads (`files`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `uploadthing` (Uploadthing)
- Docs: https://mainsdk.dev/docs/interfaces/files.md

```yaml
files:
  driver: uploadthing
```

Swap to another provider: `getqr`, `s3`, `vercel-blob`, `local`.

## Before you start

1. Create an Uploadthing app and copy its token.
2. Store it in your secrets manager as `UPLOADTHING_TOKEN`.
3. Decide the largest file and the types you accept; MainSDK enforces both on every upload URL.

## Configure

```yaml title="main.yaml"
files:
  driver: uploadthing
  key: secret:UPLOADTHING_TOKEN
  scan: true
  max-size: 16MB
```

## Use it

Issue an upload on the server, upload from the browser, then complete.

```ts title="app/api/uploads/route.ts"
import { auth, files } from "mainsdk";

export async function POST(req: Request) {
  const session = await auth.require(req);
  const upload = await files.uploadUrl({
    owner: session.user,
    maxSize: "8MB",
    types: ["image/png", "image/jpeg", "image/webp"],
  });
  return Response.json(upload);
}
```

```ts title="app/api/uploads/[id]/complete/route.ts"
import { auth, files } from "mainsdk";

export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const session = await auth.require(req);
  const { id } = await params;                           // upload.ref.id
  const ref = await files.complete(id, { owner: session.user }); // only the uploader can finish it
  const url = await files.url(ref, { for: session.user, expiresIn: "1h" });
  return Response.json({ url });
}
```

The browser sends the file to `upload.url` with `upload.method`. The browser function, the Hono version and the offline test are in [Upload files with S3, Uploadthing or Vercel Blob](https://mainsdk.dev/docs/guides/file-uploads).

Every file is stored private under your file id as its `customId`, and read through a signed URL that expires. The driver cannot meet two guarantees, and `mainsdk check` reports each as a `MAIN-FILES-0016` warning:

- `upload-single-use`: an Uploadthing presigned upload URL can be used more than once until it expires (MainSDK's own targets last 15 minutes).
- `data-region`: Uploadthing chooses where a file lives, so it cannot be kept in the tenant region. Use `s3` for EU and India tenants.

An interrupted upload is retried with the same `idempotencyKey`; Uploadthing's ingest takes the file in one request, so a retry sends it again and one file exists.

## Local development and testing

Use the `local` driver in development and `createTestApp()` from `@mainsdk/testing` in tests. Neither needs an Uploadthing account.

## What MainSDK adds on top

- Owner-scoped reads and short-lived URLs on every file.
- A scan before a file is readable, and idempotent uploads.
- `mainsdk check` fails if route code imports `uploadthing` directly; `files.raw()` gives you its client and is listed as outside the guarantees.

## Verify

Run `npx mainsdk check --env prod`. A pass exits 0 and names no `MAIN-FILES` or `MAIN-CONFIG` finding.

## If it fails

| Code | Cause | Fix |
| --- | --- | --- |
| `MAIN-FILES-0002` | `files.url()` was called for a reader `access` denies (403). | Read as the owner, or add an `access` rule for the reader. |
| `MAIN-FILES-0005` | The scan in `files.complete()` found an infected file. The upload fails and the file is quarantined. | Ask the user for a clean file. Nothing to fix in code. |
| `MAIN-FILES-0010` | `mainsdk check` found `uploadthing` imported in route code. | Use `files.uploadUrl()` and `files.complete()`, and keep the store as the `files` driver. |
| `MAIN-FILES-0016` | `mainsdk check` named a guarantee Uploadthing cannot meet (a warning). | Read the gap in the message; use `s3` or `getqr` if you need it. |
| `MAIN-CONFIG-0004` | `key` is a literal value, not a `secret:` reference. | Move the value to your secrets manager and write `secret:UPLOADTHING_TOKEN`. |

## For agents

```text
Goal: use uploadthing as the files driver and add owner-scoped uploads to this app.
Read: docs/specs/files-share.md, this page, main.yaml.
Touch only: main.yaml, main.env.dev.yaml, app/api/uploads/**, one test file.
Do: set files.driver to uploadthing with key: secret:UPLOADTHING_TOKEN;
issue uploads with files.uploadUrl({ owner, maxSize, types });
send the file to upload.url with upload.method; call files.complete(upload.ref.id, { owner }) as the same user, then files.url(ref, { for, expiresIn }).
Verify: npx mainsdk check --json --agent
Never weaken, skip or loosen a check to make it pass.
Never import uploadthing in route code.
```

## Swap it

Change `driver` to `s3`, `vercel-blob` or `getqr`. Your calls stay the same. See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).
