---
title: "Vercel Blob"
description: "Vercel Blob as the planned MainSDK files driver for apps on Vercel, with setup, main.yaml, a direct upload example and how privacy is kept."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/vercel-blob"
updated: "2026-10-28"
---

# Vercel Blob

> Vercel Blob as the planned MainSDK files driver for apps on Vercel, with setup, main.yaml, a direct upload example and how privacy is kept.

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

Vercel Blob is object storage on Vercel. The `vercel-blob` driver for [`files`](https://mainsdk.dev/docs/interfaces/files) keeps your uploads next to your app and adds owner scope, scanning and short-lived URLs on top. You would pick it when your app already runs on Vercel and you want no separate bucket to manage.

> **Note:** `files` is planned for 0.3 and this driver is built with it (spec `files-share`, ADR-0013). The steps below are the planned design; the commands do not run yet.

## Interfaces it serves

### File uploads (`files`)

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

```yaml
files:
  driver: vercel-blob
```

Swap to another provider: `getqr`, `s3`, `uploadthing`, `local`.

## Before you start

1. Create a Blob store in your Vercel project.
2. Keep its read-write token as `BLOB_READ_WRITE_TOKEN` in your secrets manager or Vercel environment, never in code.
3. Deploy to Vercel with the guide in [Deploy on Vercel](https://mainsdk.dev/docs/guides/deploy-vercel).

## Configure

```yaml title="main.yaml"
files:
  driver: vercel-blob
  key: secret:BLOB_READ_WRITE_TOKEN
  scan: true
  max-size: 25MB
```

## Use it

Issue an upload on the server. The browser sends the file straight to the store, which also covers files larger than a serverless request body allows.

```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: "25MB",
    types: ["application/pdf", "image/png", "image/jpeg"],
  });
  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).

A public blob URL is never returned for a private file. If the store can only serve a file on a public URL, the driver declares that gap in its capability profile and `mainsdk check` names it. Whether the driver can keep a blob private is not defined yet [TBD].

## Local development and testing

Use the `local` driver in development and `createTestApp()` from `@mainsdk/testing` in tests. Neither needs a Blob store.

## 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 `@vercel/blob` 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 `@vercel/blob` imported in route code. | Use `files.uploadUrl()` and `files.complete()`, and keep the store as the `files` driver. |
| `MAIN-CONFIG-0004` | `key` is a literal value, not a `secret:` reference. | Move the value to your secrets manager and write `secret:BLOB_READ_WRITE_TOKEN`. |

## For agents

```text
Goal: use vercel-blob 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 vercel-blob with key: secret:BLOB_READ_WRITE_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 @vercel/blob in route code.
```

## Swap it

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