Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Vercel Blob

Vercel Blob. Prefers auto-rotating Vercel OIDC (VERCEL_OIDC_TOKEN + BLOB_STORE_ID), falls back to BLOB_READ_WRITE_TOKEN, or pass credentials manually.

Installation

@vercel/blob is an optional peer dependency of files-sdk - install alongside the SDK so the adapter’s imports resolve at runtime.

npm install files-sdk @vercel/blob
pnpm add files-sdk @vercel/blob
yarn add files-sdk @vercel/blob
bun add files-sdk @vercel/blob
nub add files-sdk @vercel/blob
aube add files-sdk @vercel/blob

Usage

On Vercel, the adapter prefers Vercel’s OIDC authentication when VERCEL_OIDC_TOKEN and BLOB_STORE_ID are present (both are auto-injected when the Blob store is connected to the project). OIDC tokens rotate automatically, so they remove the risk that a long-lived secret leaks from the codebase or environment. Off Vercel - or if OIDC isn’t configured - the adapter falls back to BLOB_READ_WRITE_TOKEN. An explicit token option always wins.

import { Files } from "files-sdk";
import { vercelBlob } from "files-sdk/vercel-blob";

// On Vercel: VERCEL_OIDC_TOKEN + BLOB_STORE_ID are auto-injected when the
// Blob store is connected to the project (OIDC, recommended). Off Vercel,
// or as a fallback, BLOB_READ_WRITE_TOKEN is used.
const files = new Files({ adapter: vercelBlob() });

Pass oidcToken and storeId directly for runtimes that don’t expose process.env (Vite, etc.), or to bypass env detection entirely:

// Frameworks that don't load .env.local into process.env (Vite, etc.)
// need OIDC credentials passed explicitly.
const files = new Files({
  adapter: vercelBlob({
    oidcToken: loadOidcToken(),
    storeId: loadStoreId(),
  }),
});

downloadTimeoutMs bounds the body reads issued by download() and the lazy bodies returned from head()/list() (the public-URL fetch, or blob.get() in private mode). Defaults to 5 minutes; pass 0 to disable. A hung CDN response would otherwise leak a fetch that never resolves.

access selects public or private blobs and is fixed at construction. Defaults to "public". With access: "private", uploads use Vercel’s private mode and reads route through blob.get() with whichever credentials the adapter resolved (OIDC or read-write token) instead of a public URL fetch. There is no permanent public URL for private blobs, so url() mints a Vercel Signed URL instead - a presigned GET scoped to that key that expires after expiresIn seconds (1 hour by default, defaultUrlExpiresIn changes the default). Need both? Use two adapters.

Signed URLs

Vercel Signed URLs (@vercel/blob 2.4.0+) back two methods:

  • url() in private mode issues a signed token scoped to the key and the get operation, expiring with the URL, then signs the URL locally. One control-API round trip per call. Vercel caps the lifetime at 7 days server-side.
  • signedUploadUrl() in either mode returns a presigned PUT. contentType and maxSize are enforced by Vercel’s CDN, so they are real constraints, not advisory headers. There is no minimum-size counterpart: a positive minSize throws rather than handing out a URL that accepts the empty upload you asked to reject. The adapter’s addRandomSuffix and allowOverwrite settings apply to the upload.
const files = new Files({ adapter: vercelBlob({ access: "private" }) });

// Presigned GET, valid for five minutes.
const url = await files.url("invoices/2026-q1.pdf", { expiresIn: 300 });

// Presigned PUT the browser can use directly; Vercel rejects other
// content types and anything over 10 MB at the CDN.
const upload = await files.signedUploadUrl("uploads/photo.png", {
  contentType: "image/png",
  expiresIn: 600,
  maxSize: 10 * 1024 * 1024,
});
// → { method: "PUT", url, headers: { "Content-Type": "image/png" } }

Public blobs keep returning their permanent CDN URL from url() - a presigned copy would add a round trip without restricting anything - so capabilities.signedUrl.supported is true only in private mode. Neither the CDN URL nor a presigned URL can carry a Content-Disposition override, so responseContentDisposition throws in both modes.

Options

PropType
token?string

Long-lived read-write token. Defaults to `process.env.BLOB_READ_WRITE_TOKEN`. Environment-provided credentials are resolved for each operation so changes made after adapter construction are honored. Takes priority over OIDC even when both are present (mirrors the upstream `@vercel/blob` resolution order). For code running on Vercel, prefer leaving this unset and using OIDC instead.

Typestring
oidcToken?string

Vercel OIDC token. Defaults to `process.env.VERCEL_OIDC_TOKEN`, which Vercel populates automatically on every deployment when a Blob store is connected to the project. Environment-provided credentials are resolved for each operation so rotated OIDC tokens stay current. OIDC tokens are short-lived and auto-rotated, so they remove the risk that a long-lived `BLOB_READ_WRITE_TOKEN` leaks from your codebase or environment. To activate OIDC, **both** `oidcToken` and `storeId` must be available (option or env) and `token` must be unset — that matches the upstream SDK's resolution order. Pass `oidcToken` explicitly when your framework doesn't load `.env.local` into `process.env` automatically (Vite, etc.) — the adapter would otherwise silently fall back to the read-write token.

Typestring
storeId?string

Blob store id, used with OIDC. Defaults to `process.env.BLOB_STORE_ID`. Accepted in either `store_<id>` or `<id>` form (mirrors the SDK). Independently powers the `url()` fast path: when a `storeId` is known (from option, env, or derived from a `vercel_blob_rw_<storeId>_…` token), public URLs are synthesized without a round trip if `addRandomSuffix: false`.

Typestring
access?"public" | "private"

Whether blobs uploaded by this adapter are public or private. - `"public"` (default): blobs are uploaded with `access: "public"` and reachable via their CDN URL without authentication. `url()` returns a permanent public URL. - `"private"`: blobs are uploaded with `access: "private"`. They cannot be fetched by their plain URL — `download()` and the lazy bodies returned from `head()` / `list()` instead route through `blob.get(key, { access: "private" })`, which uses whichever credentials the adapter resolved (read-write token or OIDC). `url()` mints a presigned GET URL (Vercel Signed URLs) that expires after `expiresIn` seconds. `signedUploadUrl()` mints a presigned PUT URL in either mode. The setting is fixed at construction so a single `Files` instance is unambiguously one or the other. If you need both, instantiate two adapters.

Type"public" | "private"
defaultUrlExpiresIn?number

Default expiry, in seconds, for the presigned URLs `url()` mints in `access: "private"` mode. Defaults to 3600 (1 hour). Per-call `url(key, { expiresIn })` overrides. Vercel caps signed-URL lifetime at 7 days server-side. Ignored in `"public"` mode, where `url()` returns the permanent CDN URL.

Typenumber
addRandomSuffix?boolean

Add a random suffix to uploaded keys (Vercel default). When `false`, the resulting pathname matches the key 1:1, which keeps the API consistent with S3/R2 where callers expect to control the key. Defaults to `false`.

Typeboolean
allowOverwrite?boolean

Allow overwriting existing keys on upload. Defaults to `true` so that the "predictable keys" behavior (`addRandomSuffix: false`) actually works — Vercel rejects same-pathname uploads otherwise. **Trade-off:** with the defaults, an `upload(key, ...)` call silently clobbers any existing object at `key`. If keys are derived from untrusted input or your callers expect "create-only" semantics, set `allowOverwrite: false` and handle the resulting Conflict (Vercel reports the existing blob as a bad request, which the adapter maps to `Conflict`). Applies to `upload()` (including resumable/multipart uploads), `copy()`, and `signedUploadUrl()`.

Typeboolean
downloadTimeoutMs?number

Timeout in milliseconds for public-URL fetches issued by `download()`, and by lazy bodies returned from `head()`/`list()`. A hung CDN response would otherwise leak a fetch that never resolves. Defaults to 300_000 (5 minutes). Pass `0` to disable the timeout (not recommended in server contexts — a stuck request will pin a connection until the runtime tears it down).

Typenumber

Limitations

User metadata isn’t supported by the underlying API, so passing a non-empty metadata throws rather than silently dropping it. cacheControl is supported through its max-age directive, which maps to the blob’s cacheControlMaxAge; Vercel writes the rest of the header itself. A value with no max-age (e.g. "no-store") throws rather than being dropped.

With allowOverwrite: false, uploading to an existing key throws a Conflict.

Compatibility

Public access

Method Status Notes
upload ✅
download ✅
delete ✅
list ⚠️ Listed items’ type is always application/octet-stream (the list API returns no content type); use head() for the real type.
search ✅
head ✅
exists ✅
copy ✅
url ⚠️ Returns the permanent CDN URL. expiresIn is silently ignored (the URL is public already); responseContentDisposition throws (no Content-Disposition override available). Use a different provider for buckets with untrusted user-uploaded content.
signedUploadUrl ✅ Presigned PUT via Vercel Signed URLs. contentType and maxSize are enforced at the CDN; a positive minSize throws.

Private access

Method Status Notes
upload ✅
download ⚠️ Reads go through blob.get(), which has no range primitive, so range throws.
delete ✅
list ⚠️ Listed items’ type is always application/octet-stream (the list API returns no content type); use head() for the real type.
search ✅
head ✅
exists ✅
copy ✅
url ⚠️ Presigned GET via Vercel Signed URLs, honoring expiresIn (1 hour default, 7-day server-side cap). responseContentDisposition throws (no Content-Disposition override available).
signedUploadUrl ✅ Presigned PUT via Vercel Signed URLs. contentType and maxSize are enforced at the CDN; a positive minSize throws.

Was this page helpful?