The smallest adapter in the set, and the one with a caveat that belongs in a deployment decision rather than in code.
Every blob is world-readable
Read this before choosing it
Vercel Blob has one access mode: public. The URL it returns is permanent,
unguessable and unauthenticated. Anyone who obtains it — a copied <img src>, a
browser extension, a proxy log, a forwarded email — can fetch that asset
forever, with no reference to who they are.
The Media Library is otherwise private by default: every raw read checks workspace membership, and a non-member gets the same 404 as a missing asset. This backend cannot uphold that for anyone holding the underlying URL.
The app keeps enforcing its own rules — the API returns its own route, never the blob URL — but a second, public copy of the bytes exists and cannot be revoked short of deleting the blob.
Fine for a marketing site’s images. Not fine for a workspace whose media is confidential. No adapter can paper over that.
Install
npm install @apograph/media-provider-vercel-blobimport { createVercelBlobStorageProvider } from '@apograph/media-provider-vercel-blob';
MediaServerPlugin({
provider: createVercelBlobStorageProvider(config.plugins.media.storage),
config: config.plugins.media
});Configuration
{ token?, keyPrefix?, api? } — and on Vercel itself, nothing at all.
| Variable | What it does |
|---|---|
BLOB_READ_WRITE_TOKEN | Only needed off Vercel; the SDK reads it there itself |
api is the injection seam. @vercel/blob exports free functions rather than a
client, so the seam is the three-function interface the provider accepts — the
same one a caller could substitute to route through their own transport.
directUrl is false, deliberately
The store’s URL never expires and carries no per-request disposition. The port’s
directUrl promises both — a short lifetime, and a pinned
Content-Disposition and Content-Type — so declaring it would be a lie that
hands out an unrevocable link and serves an uploaded .html inline from the
blob host.
Proxying also keeps the membership check on the request path, which is the library’s actual access rule.
This is the clearest case in the codebase of a capability being a claim about what a backend can honour, not a wish.
What not to lose in a refactor
addRandomSuffix: false. The pathname this adapter computes is the
storage key. With the SDK’s default suffix on, the blob lands somewhere the
adapter cannot name, and every later read or remove misses.
The upload and the pipeline are awaited together. Awaiting the upload first meant that when it rejected, the pipeline’s own rejection had no handler yet — an unhandled rejection, which in Node kills the process rather than failing the request. The contract suite’s mid-stream-failure case catches this, and did.