Skip to content

Storage

Next.js boilerplate with Vercel Blob

File storage inside your Vercel project. One env var to wire, no second vendor.

Object storage on the platform you already deploy to, in a private Blob store. Client uploads go straight from the browser to Blob after an authorisation check. Each client token is locked to one pathname, one content type and one size. Downloads use short-lived signed URLs, and the dropzone has progress and cancel.

What Vercel Blob adds to the agent layer: 2 rules · 2 skills · 7 solution docs

Maintained by @raviMITNext.js on Vercel

From the manifest

Should you pick Vercel Blob?

Pick it if

Apps already on Vercel that want user uploads (avatars, attachments, imports) without adding a cloud account, IAM, CORS rules or a second bill. Strongest when files are small to medium and read by their owner.

Watch out for

  • Egress is billed. Blob data transfer is cheaper than Vercel's regular CDN transfer, but it is not zero. Large media served at volume is the case R2 exists for.
  • A store is private or public forever. You pick at creation and cannot change it. Mixing both means two stores.
Show 4 more
  • Advanced operations are the expensive line. Every put, copy and list counts, and so does browsing the store in the Vercel dashboard.
  • Authorisation is your code. Blob has no row level security. The upload route decides who may write and where, every time.
  • onUploadCompleted is a webhook from Vercel to your app. It cannot reach localhost, so local testing needs a tunnel.
  • Overwrites and deletes take up to 60 seconds to leave the cache. Treat blobs as immutable and give every version a new pathname.

What it costs

Hobby: free up to 1 GB stored, 10,000 simple and 2,000 advanced operations, and 10 GB of transfer a month. Past the limit, Blob stops until the 30 days reset. Pro bills by usage. Prices below are for iad1. Storage is $0.023 per GB-month and transfer $0.05 per GB. Simple operations (cache-miss reads) are $0.40 per million. Advanced operations (put, copy, list) are $5 per million. Deletes are free. Client uploads have no transfer charge.

Prices change. Check with Vercel Blob before you commit.

registry/tested.yaml

Tested with Vercel Blob

Each pair was installed, typechecked, linted, built and booted together.

Database
NeonSupabase
Admin panel
Admin panel
Error tracking
Sentry
Customer support
Crisp

What it adds

What Vercel Blob adds to the repo

Read straight from the vercel-blob manifest, so it is exactly what lands in your repo.

Environment variables

  • BLOB_READ_WRITE_TOKENRequired

    Read-write token for the Blob store. Vercel adds it to the project when you connect a store, and vercel env pull copies it locally. The upload route needs it to sign client tokens, even when OIDC covers the rest. Full access to every blob in the store: server-side only, never NEXT_PUBLIC_.

    Where to get it
    Vercel dashboard -> Storage -> your Blob store -> Settings (or vercel env pull after connecting the store to the project)
    Placeholder
    vercel_blob_rw_0123456789abcdef_0123456789abcdef0123456789abcdef
  • VERCEL_BLOB_CALLBACK_URLOptional

    Local only, and empty by default on purpose. Example value: https://abc123.ngrok-free.app. Vercel Blob calls onUploadCompleted over the internet, so it cannot reach localhost. Point this at a tunnel to your dev server to test the callback. Leave it unset on Vercel: the SDK derives the URL from the deployment.

    Where to get it
    Start a tunnel (ngrok http 3000, or cloudflared) and paste its https origin
  • STORAGE_DEV_UPLOADEROptional

    Local-only escape hatch. Set it to any id and uploads are attributed to that id, so the dropzone works before resolveUploader() is wired to an auth battery. Ignored when NODE_ENV is production.

    Where to get it
    Set it in .env.local only. Never in the hosting provider's environment.
    Placeholder
    dev-user
  • STORAGE_ALLOWED_ORIGINSOptional

    Extra origins allowed to request an upload, comma separated. NEXT_PUBLIC_APP_URL and the current Vercel deployment URL are always allowed and do not belong here.

    Where to get it
    Your own domains. Vercel preview URLs are allowed automatically.
    Placeholder
    https://app.example.com,https://staging.example.com

Dependencies

  • @vercel/blob^2.8.0
  • server-only^0.0.1

Files it writes

13 files, at these exact paths.

  • src/7 files
    • app/1 file
      • api/1 file
        • upload/1 file
          • route.ts
    • components/1 file
      • upload/1 file
        • file-dropzone.tsx
    • lib/5 files
      • storage/5 files
        • access.ts
        • authorize.ts
        • confirm.ts
        • index.ts
        • keys.ts
  • tests/1 file
    • unit/1 file
      • storage-keys.test.ts
  • variants/5 files
    • auth-clerk/1 file
      • slots/1 file
        • auth-public-routes.ts
    • auth-none/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • uploader.ts
    • auth-wired/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • uploader.ts
    • errors-none/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • report.ts
    • errors-sentry/1 file
      • src/1 file
        • lib/1 file
          • storage/1 file
            • report.ts

Stack slots it fills

The stack declares these injection points; this battery supplies the fragment, so the provider tree, the env check and the schema stay one file each instead of many.

  • @slot env-required
  • @slot legal-processors
  • @slot verify-checks

The differentiator

What Vercel Blob teaches your agent

Other starter kits stop at the package. This is the part an agent reads: where it may work, what it must never do there, and the problems someone already solved.

Rules (2)

Loaded when the agent opens a matching file.

Vercel Blob store - access mode, credentials, cache, cost

Loads onsrc/lib/storage/**scripts/verify.ts.claude/rules/blob-store.md
Access mode is fixed at creation
  • A store is private or public, forever. BLOB_ACCESS in src/lib/storage/access.ts must match it. Every put() and upload() names the mode, and a mismatch fails.
  • User files go in a private store. Reads use getSignedDownloadUrl(), a presigned GET that expires in 15 minutes by default.
  • Public stores are for things that are public by nature. The URL never expires and the pathname is the only secret.
  • Need both? Two stores. Never try to mix them in one.
Credentials
  • BLOB_READ_WRITE_TOKEN is required. handleUpload() signs client tokens with it. It can read, overwrite and delete every blob: server-side only, never NEXT_PUBLIC_.
  • On Vercel the SDK also gets OIDC credentials and prefers them for server reads and writes. Do not pass token to put(), del() or get() unless you mean to bypass OIDC.
  • src/lib/storage/index.ts starts with import "server-only". Keep it. The dropzone may import only access.ts, keys.ts and report.ts.
  • Never read an env var at module scope. next build runs with none set.
Treat blobs as immutable
  • Overwrites and deletes take up to 60 seconds to leave the CDN cache, and browsers keep their own copy for cacheControlMaxAge (one month by default).
  • New content means a new pathname. Store the new key, delete the old blob.
  • useCache: false on get() reads from origin, but costs Fast Origin Transfer on every call. Only for data that must be fresh.
Costs to keep in mind
  • put, copy and list are advanced operations, the priciest line. Never list() on a request path. Keep keys in your database instead.
  • Browsing the store in the Vercel dashboard counts as operations too.
  • del() is free but counts toward the rate limit (per blob, not per call).
  • issueSignedToken() is a network call. getSignedDownloadUrl() caches one store-wide read token and signs URLs locally. Keep it that way.
  • Serving a private blob through a function pays twice: Blob transfer into the function, Fast Data Transfer out. Prefer the presigned URL.
Deleting
  • Prove ownership first. These credentials can delete anything.
  • Delete the blob, then the row. An orphaned blob costs storage. An orphaned row breaks a page.
  • deleteObject() treats "already gone" as success, so cleanup jobs can retry.

Client uploads - auth before the token, the server names the key, the token is locked down

Loads onsrc/lib/storage/**src/app/api/upload/**src/components/upload/**.claude/rules/client-uploads.md
A client token is a write credential

Whoever holds one can write to your store until it expires. No session, no cookie, no further check.

  • Every path that issues a token calls requireUploader(request) from @/lib/storage/authorize first. In onBeforeGenerateToken, it is the first line. Nothing after it runs for an anonymous or cross-origin caller.
  • Throw to refuse. handleUpload() issues no token when onBeforeGenerateToken throws. Returning {} issues an unrestricted one.
  • Keep validUntil short. The route uses 15 minutes. The SDK default is an hour.
  • Never log a token or a signed URL. Never put one in an analytics property or an error report. Responses carry cache-control: no-store.
Never trust the pathname from the client

upload(pathname, ...) lets the browser name the pathname. The token is bound to it, and the server can only accept or refuse it.

  • The server mints keys: objectKey({ ownerId: uploader.id, ... }) in the reserve step of src/app/api/upload/route.ts. The owner id comes from the session, never from the request body.
  • In onBeforeGenerateToken, call assertUploadPathname(pathname, uploader.id, contentType) before anything else is returned. It checks the owner segment, the exact key shape and the extension.
  • Do not "fix up" a bad pathname. Refuse it. A token for a rewritten pathname does not exist; the SDK signs the one the client sent.
  • Before a read or a delete of a key that came from a request, call isOwnedBy(key, uploader.id). Authentication is not authorisation.
Allowlist types and sizes in the token
  • allowedContentTypes gets exactly one entry: the claimed type, already checked against ALLOWED_CONTENT_TYPES in src/lib/storage/keys.ts.
  • maximumSizeInBytes gets the claimed size, already checked against MAX_UPLOAD_BYTES. Blob enforces both at the store, so a client that lied fails there.
  • Allowlist, never blocklist. SVG stays out: it runs script when served inline.
  • Change a limit in keys.ts, not in the route. The dropzone reads the same constants.
Overwrites and suffixes
  • allowOverwrite: false and addRandomSuffix: false, always, in the upload route. The key already holds a uuid. A suffix changes the pathname the browser expects. An overwrite lets a leaked token replace a file.
  • Need a new version of a file? New key. Then delete the old one.
User files never go through a function
  • Vercel Functions cap the request body at 4.5 MB, bill every second of the transfer, and add Fast Data Transfer to every byte. Client uploads have none of that.
  • putObject() is for bytes your own code makes: exports, thumbnails, PDFs. Never for a file a user is sending.
  • Never expose a storage helper as a server action that takes a key without an ownership check. A server action is a public endpoint.
onUploadCompleted
  • It is a webhook from Vercel to your deployment. It does not fire on localhost unless VERCEL_BLOB_CALLBACK_URL points at a tunnel.
  • handleUpload() verifies its signature with BLOB_READ_WRITE_TOKEN. Do not run requireUploader() on it: the caller is Vercel, and it sends no Origin.
  • Make it idempotent. A non-2xx answer makes Vercel retry.
  • Trust tokenPayload, which the server signed. Never trust clientPayload beyond the checks in onBeforeGenerateToken.

Skills (2)

Invoked by name.

  • /add-upload-kind

    Add a new kind of upload (avatars, attachments, imports) to the Vercel Blob flow, with its own folder, type and size limits, the row that owns the key, and the cleanup that stops orphans.

    .claude/skills/add-upload-kind/SKILL.md

  • /clean-orphaned-blobs

    Find and delete Vercel Blob objects no database row points at, safely, in batches, without deleting uploads that are still in flight.

    .claude/skills/clean-orphaned-blobs/SKILL.md

Solution docs (7)

Written before you hit the problem. Each one ships in the repo at docs/solutions/ and is published here as a cookbook page.

Show all 7

How it fits

What Vercel Blob needs, and what it goes well with

The resolver enforces this before it generates anything, and names every addition it makes.

Requires

Nothing. Vercel Blob stands on its own.

Pairs well with

  • An auth battery. Suggested, never added for you.

Cannot be combined with

No hard conflicts.

Build a repo with Vercel Blob

Free and MIT. The builder opens with Vercel Blob picked. You download the zip right away, and we email you the link too.