Runtime: Beignet requires Node.js 22.12 or newer. Bun is optional.
Vercel Blob object storage provider for Beignet
Beignet is experimental alpha software. The 0.0.x package line is for early
evaluation, and APIs may change between releases while the framework settles.
This package adapts Vercel Blob to
Beignet's StoragePort, so apps deployed on Vercel get durable object storage
without hand-writing an adapter.
createVercelBlobStorageProvider(...) returns the stable
VercelBlobStorageProvider type. VercelBlobConfig describes its validated
config; the Zod schema remains internal.
bun add @beignet/provider-storage-vercel-blob @vercel/blob@^2.8.0
@vercel/blob 2.8.0 or newer within the v2 line is required.
Or scaffold everything — dependencies, provider wiring, ports, env example, and setup notes — with the CLI preset:
bun beignet provider add storage-vercel-blob
// server/providers.ts
import { createVercelBlobStorageProvider } from "@beignet/provider-storage-vercel-blob";
export const providers = [
// ...
createVercelBlobStorageProvider(),
] as const;
The provider contributes ports.storage (the standard Beignet StoragePort)
and ports.vercelBlob (an escape hatch, see below). Configuration comes from
BLOB_* env vars and, for OIDC authentication, VERCEL_OIDC_TOKEN:
| Env var | Meaning |
|---|---|
BLOB_READ_WRITE_TOKEN |
Read-write token. Set automatically on Vercel deployments with a connected Blob store; set it manually elsewhere. |
BLOB_ACCESS |
Store-wide access level: private (default) or public. |
BLOB_KEY_PREFIX |
Optional prefix applied to every object key. |
BLOB_STORE_ID |
Optional store id when authenticating with a Vercel OIDC token. |
VERCEL_OIDC_TOKEN |
Vercel-provided OIDC token used with BLOB_STORE_ID instead of a read-write token. |
Local storage keeps working with zero setup by switching on the available credentials, the same way environment-swapped rate-limit providers work:
import { createLocalStorageProvider } from "@beignet/provider-storage-local";
import { createVercelBlobStorageProvider } from "@beignet/provider-storage-vercel-blob";
const hasVercelBlobCredentials =
Boolean(process.env.BLOB_READ_WRITE_TOKEN) ||
Boolean(process.env.BLOB_STORE_ID && process.env.VERCEL_OIDC_TOKEN);
export const providers = [
// Vercel Blob in deployed environments (local disk does not survive
// serverless); the local provider keeps dev working with zero setup.
hasVercelBlobCredentials
? createVercelBlobStorageProvider()
: createLocalStorageProvider(),
] as const;
Vercel Blob stores per-object access at write time but does not report it
back from head(...), so this adapter keeps the whole store uniform: every
object shares the configured access level, and a put(...) that requests a
different visibility is rejected with an error instead of silently
misreporting visibility on later reads. Run two providers over two Blob
stores when an app needs both levels.
publicUrl(...) returns the blob URL for public stores and null for
private stores. Private objects should be served through app routes that
enforce the app's own authorization, then stream the body from
ctx.ports.storage.get(...).
Vercel Blob has no arbitrary per-object metadata, so StorageObject.metadata
is always reported empty. Cache lifetimes map from the Cache-Control
max-age directive to Vercel Blob's cacheControlMaxAge seconds; other
directives do not survive the mapping.
Use the direct factory when you own provider wiring or need a second store:
import { createVercelBlobStorage } from "@beignet/provider-storage-vercel-blob";
const storage = createVercelBlobStorage({
access: "public",
keyPrefix: "avatars",
storeId: process.env.AVATARS_BLOB_STORE_ID,
token: process.env.AVATARS_BLOB_TOKEN,
});
Omit token to let @vercel/blob read BLOB_READ_WRITE_TOKEN from the
environment. storeId is forwarded explicitly to every SDK operation for
OIDC-authenticated stores; the SDK reads VERCEL_OIDC_TOKEN, and the env-backed
provider maps BLOB_STORE_ID to the same option. The client option accepts a
fake SDK surface for tests.
beignet doctor --strict and beignet preflight accept either the read-write
token or the complete BLOB_STORE_ID plus VERCEL_OIDC_TOKEN pair.
Some StoragePort promises need an extra Blob API round-trip:
put(...) issues a post-write head(...) because the port returns the
authoritative stored object, not an echo of inputs.delete(...) issues a pre-delete head(...) because Vercel Blob's
del(...) does not report whether the object existed.ports.vercelBlob exposes the raw pieces for integrations the port does not
cover:
const { client, access, keyPrefix, storeId, objectKey, checkHealth } =
ctx.ports.vercelBlob;
await client.list({ prefix: objectKey("exports"), limit: 100 });
const health = await checkHealth();
checkHealth() issues a one-item list(...) and reports { ok, message?, metadata } for readiness endpoints.
Object keys and keyPrefix use the shared validation and prefix helpers from
@beignet/core/ports, keeping Blob behavior aligned with memory, local, and
S3 storage.
get(...) returns a one-shot response body. Consume one body method, or call
await object.cancel() when only inspecting metadata so the unread response
releases its resources. Prefer stat(...) for metadata-only reads.
When @beignet/devtools is installed before this provider, storage
operations appear under the dashboard's Storage watcher. The provider records
storage.put, storage.get, storage.stat, storage.delete,
storage.exists, and storage.publicUrl events with key, size, and
duration; failures are recorded with .failed event names and the original
error is rethrown.
Missing objects resolve to null (get, stat, publicUrl) or false
(delete, exists) — BlobNotFoundError never escapes the port. All other
SDK errors are rethrown unchanged after an instrumentation event, including
missing-token errors, so misconfiguration fails loudly rather than
pretending storage is empty.
This package covers server-mediated storage through StoragePort. Vercel
Blob's browser-direct uploads use its own client-token protocol
(@vercel/blob/client), which does not fit Beignet's plain presigned-PUT
UploadSignerPort; use createS3UploadSigner from
@beignet/provider-storage-s3 when direct uploads are a requirement, or
Vercel's client SDK directly.
Keep dev and tests on @beignet/provider-storage-local or
createMemoryStorage() from @beignet/core/ports; both implement the same
port. Inject a fake VercelBlobClient through the client/createClient
options to test app code against this adapter without network access.
Connect a Blob store to the Vercel project and configure one credential path:
use BLOB_READ_WRITE_TOKEN, or use Vercel OIDC with VERCEL_OIDC_TOKEN plus
BLOB_STORE_ID. Doctor, provider audit, and preflight validate those
alternatives without treating the read-write token as the only valid path; run
beignet preflight --connect to prove the configured store is reachable.
Prefer private stores unless objects are genuinely world-readable: public
blob URLs are unauthenticated and long-cached.