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 moreShow fewer
- 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.
- 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 pullcopies 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 pullafter 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
onUploadCompletedover 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_ACCESSinsrc/lib/storage/access.tsmust match it. Everyput()andupload()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_TOKENis required.handleUpload()signs client tokens with it. It can read, overwrite and delete every blob: server-side only, neverNEXT_PUBLIC_.- On Vercel the SDK also gets OIDC credentials and prefers them for server
reads and writes. Do not pass
tokentoput(),del()orget()unless you mean to bypass OIDC. src/lib/storage/index.tsstarts withimport "server-only". Keep it. The dropzone may import onlyaccess.ts,keys.tsandreport.ts.- Never read an env var at module scope.
next buildruns 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: falseonget()reads from origin, but costs Fast Origin Transfer on every call. Only for data that must be fresh.
Costs to keep in mind
put,copyandlistare advanced operations, the priciest line. Neverlist()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/authorizefirst. InonBeforeGenerateToken, it is the first line. Nothing after it runs for an anonymous or cross-origin caller. - Throw to refuse.
handleUpload()issues no token whenonBeforeGenerateTokenthrows. Returning{}issues an unrestricted one. - Keep
validUntilshort. 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 ofsrc/app/api/upload/route.ts. The owner id comes from the session, never from the request body. - In
onBeforeGenerateToken, callassertUploadPathname(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
allowedContentTypesgets exactly one entry: the claimed type, already checked againstALLOWED_CONTENT_TYPESinsrc/lib/storage/keys.ts.maximumSizeInBytesgets the claimed size, already checked againstMAX_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: falseandaddRandomSuffix: 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_URLpoints at a tunnel. handleUpload()verifies its signature withBLOB_READ_WRITE_TOKEN. Do not runrequireUploader()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 trustclientPayloadbeyond the checks inonBeforeGenerateToken.
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.
- Vercel Blob addRandomSuffix, allowOverwrite, and stale files in the cacheBlob refuses to overwrite by default, and an overwrite can take 60 seconds to show plus whatever the browser cached. Treat blobs as immutable; use new pathnames, not overwrites.docs/solutions/vercel-blob/addrandomsuffix-allowoverwrite-and-the-cache.md
- Vercel Blob: client uploads vs server uploads and the 4.5 MB limitA Vercel Function accepts at most 4.5 MB of request body. Server uploads hit it; client uploads skip it. How client uploads work, and when a server upload is still right.docs/solutions/vercel-blob/client-uploads-and-the-4-5-mb-body-limit.md
- Deleting orphaned blobs in Vercel BlobBlobs nobody references still bill every month. Where orphans come from, how to delete them in the same code path as the row, and a safe sweep with list() and del() for the rest.docs/solutions/vercel-blob/deleting-orphaned-blobs.md
- Vercel Blob handleUpload: never trust the pathname from the clientWith client uploads the browser names the pathname and the token is bound to it. Check it in onBeforeGenerateToken, or any signed-in user can write anywhere in your store.docs/solutions/vercel-blob/never-trust-the-client-pathname.md
- Why Vercel Blob onUploadCompleted never fires on localhostonUploadCompleted is a webhook from Vercel to your app. It cannot reach localhost, so the SDK skips it. Use a tunnel and VERCEL_BLOB_CALLBACK_URL, and never make the upload depend on it.docs/solutions/vercel-blob/onuploadcompleted-never-fires-on-localhost.md
Show all 7Show fewer
- Private vs public Vercel Blob stores: picking one you cannot changeA Blob store's access mode is fixed at creation. Private needs a credential for every read; public is readable by anyone with the URL. How to serve each, and why user files belong in private.docs/solutions/vercel-blob/private-vs-public-blob-stores.md
- What Vercel Blob actually costs, and where egress hidesStorage is cheap. Advanced operations and data transfer are the lines that grow. How each is counted, why private delivery can cost more, and the habits that keep the bill flat.docs/solutions/vercel-blob/vercel-blob-costs-and-egress.md
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.
Compared with the alternatives
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.