Set up Vercel Blob authentication: OIDC, local development, and token fallback
Which credential the files-sdk Vercel Blob adapter uses on Vercel Functions, in local development, and on other hosts, and how to fix each setup error.
With a Blob store connected to your Vercel project, vercelBlob() needs no credentials in your code. The adapter hands @vercel/blob the store ID from BLOB_STORE_ID, and @vercel/blob finds an OIDC token on every call: the request’s x-vercel-oidc-token header inside a Vercel Function, VERCEL_OIDC_TOKEN during builds and local development. One Files instance created at module scope works in all three places.
The catch is the fallback. When @vercel/blob finds no usable OIDC token, it uses BLOB_READ_WRITE_TOKEN without saying so, and without one the call fails with missing credentials. Most setup problems are a lookup that came up empty: a call made outside a request, a development token that couldn’t be refreshed, or a pulled .env.local running on a server outside Vercel.
Before you start
- A Vercel project with a Blob store connected to it (the project’s Storage tab). Include Development in the connection’s environments if you want to use the store from your machine.
- The Vercel CLI, logged in, for
vercel link,vercel env pull, and refreshing development tokens. - Written against files-sdk 3.0,
@vercel/blob2.8,@vercel/oidc3.8 (a dependency of@vercel/blob), and Next.js 16.4. The examples are App Router code, but nothing depends on Next.js except how.env.localgets loaded.
npm install files-sdk @vercel/blobpnpm add files-sdk @vercel/blobyarn add files-sdk @vercel/blobbun add files-sdk @vercel/blobnub add files-sdk @vercel/blobaube add files-sdk @vercel/blobWhere each credential comes from
| Where the code runs | What Vercel provides | What a module-scope vercelBlob() uses |
|---|---|---|
| A Vercel Function handling a request | The OIDC token in the request’s x-vercel-oidc-token header. BLOB_STORE_ID in process.env, plus BLOB_READ_WRITE_TOKEN if the store’s token is included in that environment. |
The request’s OIDC token, even when BLOB_READ_WRITE_TOKEN is set |
A Vercel build, such as next build prerendering a page |
VERCEL_OIDC_TOKEN and BLOB_STORE_ID in process.env |
VERCEL_OIDC_TOKEN |
Your machine after vercel link and vercel env pull |
VERCEL_OIDC_TOKEN and BLOB_STORE_ID in .env.local |
VERCEL_OIDC_TOKEN, if your framework loads .env.local, refreshed when it expires |
| A server outside Vercel: CI, a container, another host | Nothing. You set BLOB_READ_WRITE_TOKEN yourself. |
BLOB_READ_WRITE_TOKEN |
Vercel’s OIDC docs describe the three delivery paths, and its system environment variables reference lists VERCEL_OIDC_TOKEN as available at build time, with the runtime token “set to the x-vercel-oidc-token header on your functions’ Request object”. Per the OIDC reference, build tokens expire after one hour, preview and production function tokens after two, and development tokens after 12. @vercel/blob checks the request header first and the variable second, so the same code covers every row.
How the adapter picks a credential
The adapter resolves credentials on every call, in this order:
- The
tokenoption. It always wins, including over OIDC. - The
oidcTokenoption, paired with thestoreIdoption orBLOB_STORE_ID. The token is used as given and never refreshed. Without a store ID the adapter throws rather than fall back, so a typo can’t quietly switch you to the read-write token. - A store ID (
storeIdorBLOB_STORE_ID) and notokenoption. The adapter passes only the store ID, and@vercel/blobpicks the credential: the request’sx-vercel-oidc-tokenheader, thenVERCEL_OIDC_TOKEN(refreshed if it has expired and a refresh is possible), thenBLOB_READ_WRITE_TOKEN. - No store ID:
BLOB_READ_WRITE_TOKEN, even inside a Vercel request, because OIDC needs a store ID.
storeId accepts store_<id> or <id>. Three consequences are easy to miss:
- A read-write token in the environment doesn’t outrank OIDC. With a store ID set,
BLOB_READ_WRITE_TOKENis the last resort. To use it on purpose, pass it astoken. - The fallback is silent. When the OIDC lookup or its refresh fails,
@vercel/blobtreats the token as absent and moves on, as its 2.5.0 changelog entry describes. The refresh’s own error isn’t reported: you see either the read-write token working, ormissing credentials. - Construction checks only what it can.
vercelBlob()throws immediately when there’s neither a store ID nor a read-write token, or whenoidcTokenhas no store ID. With a store ID alone it can’t know whether a request will bring a token, so it loads fine and a missing credential surfaces at the first call. That’s what lets a module-scope adapter load on Vercel.
Use OIDC on Vercel
Create the instance once and import it wherever you need storage:
import { createFiles } from "files-sdk";
import { vercelBlob } from "files-sdk/vercel-blob";
// The store ID comes from BLOB_STORE_ID. Each call authenticates with the
// OIDC token of the request it runs in.
export const files = createFiles({
adapter: vercelBlob({ access: "private" }),
});
import { getSession } from "@/lib/auth";
import { files } from "@/lib/files";
export async function GET(request: Request) {
const session = await getSession(request.headers);
if (!session) {
return new Response("Sign in first", { status: 401 });
}
const { items } = await files.list({ prefix: `users/${session.user.id}/` });
return Response.json(items.map(({ key, size }) => ({ key, size })));
}
getSession stands in for your auth library (Auth.js, Clerk, Better Auth, or your own cookie check). This guide assumes it takes request headers and returns { user: { id: string } } or null.
Each call in the handler reads the header of the request it runs in, so concurrent requests each use their own token. Every adapter method works this way, including the presigned URLs from url() and signedUploadUrl(), which Vercel issues through issueSignedToken with the same credentials as other calls. The gateway takes the same instance: createFilesRouter({ files, authorize }).
Calls made outside a request, such as code that runs when the module loads, have no header to read. They fall through to the environment (VERCEL_OIDC_TOKEN if the runtime has one, then BLOB_READ_WRITE_TOKEN) or fail with missing credentials. Keep storage calls inside handlers, server actions, and server components.
Set up local development
Link the directory to the Vercel project and pull its Development variables:
vercel link
vercel env pull
vercel env pull writes .env.local, including VERCEL_OIDC_TOKEN and BLOB_STORE_ID. If BLOB_STORE_ID is missing, the store’s connection doesn’t include Development: open the store’s Projects tab, choose Update Project Connection from the project’s menu, add Development, and pull again.
next dev loads .env.local into process.env, so the module-scope files works without further setup.
The pulled token expires after 12 hours. Once it has, @vercel/oidc (which @vercel/blob calls for the token) fetches a new one: it looks for the .vercel/project.json that vercel link wrote, starting in the process’s working directory and walking up, then requests a token for that project with your Vercel CLI login. It caches the token per project in your user data directory and sets it in process.env for the running process. .env.local keeps the old value. Vercel’s Blob SDK docs describe the same refresh “using your Vercel CLI credentials”.
The refresh doesn’t depend on next dev. A script refreshes too, as long as it runs inside the linked directory. It does have to load .env.local itself:
import { createFiles } from "files-sdk";
import { vercelBlob } from "files-sdk/vercel-blob";
const files = createFiles({ adapter: vercelBlob({ access: "private" }) });
const { items } = await files.list({ limit: 5 });
console.log(
"Connected. First keys:",
items.map((item) => item.key)
);
node --env-file=.env.local scripts/check-blob.ts
Run it from the project root. Node 22.18 and later run TypeScript files directly. Bun loads .env.local on its own, so bun scripts/check-blob.ts works too.
If the refresh can’t run (the working directory isn’t inside the linked project, or the CLI isn’t logged in), @vercel/blob treats the expired token as absent. The call uses BLOB_READ_WRITE_TOKEN if .env.local has one, and otherwise fails with missing credentials. Run vercel login, or vercel env pull for a fresh 12-hour token.
Use a read-write token outside Vercel
A server that isn’t on Vercel has no OIDC token, so give it BLOB_READ_WRITE_TOKEN. Vercel creates that token with the store; copy it from the project’s environment variables into your host’s secret store. It doesn’t expire.
Leave BLOB_STORE_ID unset on that server. Without a store ID, the adapter passes the read-write token straight to @vercel/blob and no OIDC lookup runs. With one, every call first tries the lookup and a refresh, finds nothing, and then falls back to the same token.
To name the secret something else, or to use two stores from one process, pass it as token. An explicit token wins over every environment variable:
import { createFiles } from "files-sdk";
import { vercelBlob } from "files-sdk/vercel-blob";
export const invoices = createFiles({
adapter: vercelBlob({
access: "private",
token: process.env.INVOICES_BLOB_TOKEN,
}),
});
Don’t deploy your .env.local to that server. Its BLOB_STORE_ID hands the choice to @vercel/blob, and its VERCEL_OIDC_TOKEN is your development token, which wins while it’s valid: for up to 12 hours after the pull, the server authenticates with the project’s Development token. Once the token expires, the refresh fails on a server with no linked project or CLI login, and calls switch to BLOB_READ_WRITE_TOKEN without an error, or fail with missing credentials if it isn’t set.
Decide whether Vercel still needs the read-write token
When you create a store, Vercel adds BLOB_READ_WRITE_TOKEN to the projects you select. With the store connected, Functions and builds authenticate with OIDC, and the adapter reaches the read-write token only when a lookup comes up empty, such as a call outside a request. Leaving it set turns that mistake into a silent switch to a credential that doesn’t expire; removing it makes the same mistake fail with missing credentials.
Keep it if something else needs it. Vercel’s own handleUpload() generates client tokens from a read-write token and throws without one. To use the read-write token on Vercel on purpose, pass token: process.env.BLOB_READ_WRITE_TOKEN, which outranks OIDC.
Troubleshooting
vercelBlob adapter: missing credentials. Pass `token`, or `oidcToken` + `storeId`, or set BLOB_READ_WRITE_TOKEN, or set BLOB_STORE_ID for OIDC (the token comes from the request's x-vercel-oidc-token header on Vercel Functions, or from VERCEL_OIDC_TOKEN). The FilesError code is Invalid, which is permanent, so retries don’t repeat it. Thrown by vercelBlob() itself, it means neither BLOB_STORE_ID nor BLOB_READ_WRITE_TOKEN is set: .env.local wasn’t loaded, or the store isn’t connected to this environment. Thrown by a call, it means the adapter had a store ID but no OIDC token turned up and there’s no read-write token. On Vercel, the call ran outside a request. Locally, the pulled token expired and couldn’t be refreshed; see Set up local development. The error’s cause is @vercel/blob’s No blob credentials found error.
vercelBlob adapter: `oidcToken` was passed but no `storeId` was found. Pass `storeId` or set BLOB_STORE_ID to use OIDC. You passed oidcToken and no store ID resolved. Pass storeId as well, or check that the store’s project connection covers this environment.
Vercel Blob: Access denied, please provide a valid token for this resource. The FilesError code is Unauthorized. Blob rejected the credential. Two common causes: a token passed as oidcToken that has expired, since Vercel’s Blob SDK docs warn that an explicitly passed token isn’t refreshed and fails with a 403 once it expires; or a read-write token from a different store.
Vercel Blob: OIDC is enabled for this project, but not for the … environment. The store’s connection to this project doesn’t include the environment the token was issued for. Add it under Update Project Connection in the store’s Projects tab, then pull or redeploy.
Vercel Blob: This store does not exist. BLOB_STORE_ID, or the store embedded in a read-write token, points at a store that was deleted or belongs to another team.