local-blobtastic
v0.2.0
Published
Local development emulator for the Vercel Blob API.
Downloads
332
Maintainers
Readme
local-blob
Local development emulator for the Vercel Blob API.
The npm package is local-blobtastic; it installs the local-blob CLI.
This package runs on Node.js only. No Bun or Docker runtime is required.
Current status
Works with @vercel/[email protected] for common local development flows. It stores objects on disk and serves a local HTTP API compatible with the Vercel Blob client when VERCEL_BLOB_API_URL points at this server.
Compatibility is tested with @vercel/[email protected].
Supported API:
- direct public blob reads with
fetch(blob.url)and download reads withfetch(blob.downloadUrl) - direct private blob
GET,HEAD, anddownloadUrlreads withAuthorization: Bearer <BLOB_READ_WRITE_TOKEN> - presigned private object
GETandHEAD - control-plane
head put, including default no-overwrite behavior,allowOverwrite,addRandomSuffix,ifMatch, cache max age, and Vercel Blob-styledownloadUrl- presigned single-part
PUT, including allowed content type, maximum size, overwrite, random suffix, cache max age, andifMatchconstraints - multipart
putvia{ multipart: true }, with the same overwrite, suffix, andifMatchbehavior as regularput - presigned multipart create, part upload, and complete
copy, includingallowOverwrite,addRandomSuffix,ifMatch, and copy-request metadata semanticscreateFolderdel, including single-URLifMatch- presigned
DELETE /?pathname=..., includingvercel-blob-if-match list, including cursor pagination andmode: 'folded'issueSignedToken()and SDK-compatible local delegation/client signing tokenspresignUrl()URLs for localget,head,put, multipartput, anddeleteworkflows- client-token uploads via
@vercel/blob/client.upload - client-token multipart uploads
- presigned browser uploads via
@vercel/blob/client.uploadPresigned - upload-completed callbacks through
handleUploadand local presigned callback handling - Vercel Blob-style JSON errors for common SDK error mapping
Run
From npm once published:
npx local-blobtasticWith options:
npx local-blobtastic --port 9966 --store .local-blob-storeOptions:
-p, --port <port> Port to listen on. Defaults to PORT or 3000.
-s, --store <path> Blob store directory. Defaults to VERCEL_STORE_PATH or .store.
-t, --token <token> Read/write token. Defaults to BLOB_READ_WRITE_TOKEN or vercel_blob_rw_localstore_nonce.
-h, --help Show help.On startup, local-blob prints the control-plane URL, object-plane URL shape, and the environment variables to add to your app, for example:
local-blob control plane listening on http://localhost:3000
local-blob object plane using http://localstore.<public|private>.localhost:3000/<pathname>BLOB_READ_WRITE_TOKEN=vercel_blob_rw_localstore_nonce
VERCEL_BLOB_API_URL=http://localhost:3000Keep VERCEL_BLOB_API_URL pointed at the control plane. Blob write responses return object-plane URLs shaped like http://<store-id>.<access>.localhost:<port>/<pathname>.
URL model
local-blob uses two local URL families:
| URL family | Shape | Purpose |
| --- | --- | --- |
| Control plane | http://localhost:<port> | SDK API calls such as put, head, list, copy, del, multipart, issueSignedToken, and presigned upload/delete control routes. |
| Object plane | http://<store-id>.<access>.localhost:<port>/<pathname> | Direct object fetch, private bearer reads, and presigned object GET/HEAD. |
Examples:
http://localstore.public.localhost:3000/avatar.pnghttp://localstore.private.localhost:3000/documents/report.pdf
*.localhost normally resolves to loopback in modern environments. If your HTTP client or test runner does not resolve wildcard localhost names, proxy the request to localhost:<port> while preserving the original object-plane host in the Host or x-forwarded-host header.
Reading blobs locally
Use the object-plane URL returned by write commands and fetch it directly:
import { put } from '@vercel/blob';
const blob = await put('hello.txt', 'Hello, World!', { access: 'public' });
// blob.url is similar to http://localstore.public.localhost:3000/hello.txt
const response = await fetch(blob.url);
const text = await response.text();Use blob.downloadUrl for download-style local reads. It is blob.url with ?download=1 and returns Content-Disposition: attachment; filename="...":
const downloadResponse = await fetch(blob.downloadUrl);Private direct reads
Private writes return private-shaped object-plane URLs such as http://localstore.private.localhost:3000/document.txt. Direct private object GET, HEAD, and downloadUrl reads require the local read-write bearer token:
const blob = await put('documents/report.txt', 'secret', {
access: 'private',
});
const response = await fetch(blob.url, {
headers: {
Authorization: `Bearer ${process.env.BLOB_READ_WRITE_TOKEN}`,
},
});
const headResponse = await fetch(blob.url, {
method: 'HEAD',
headers: {
Authorization: `Bearer ${process.env.BLOB_READ_WRITE_TOKEN}`,
},
});Missing blobs return 404; existing private blobs without valid bearer or presigned auth return 403.
SDK get() limitation
Do not use @vercel/blob.get() with the local emulator. This is an upstream SDK routing/validation limitation, not a missing object-read handler in local-blob.
The emulator serves object reads from the local object-plane URLs returned by write operations, for example http://localstore.public.localhost:3000/hello.txt. Direct fetch(blob.url) works against those URLs.
@vercel/blob.get() is different from fetch: it assumes Vercel's production object URL model. In @vercel/[email protected], get(blob.url, ...) rejects local *.localhost object URLs because they are not *.blob.vercel-storage.com, and get('hello.txt', ...) builds a production Vercel Blob URL rather than routing through VERCEL_BLOB_API_URL. In both cases, the failed request is decided inside the SDK before the local emulator can handle it.
Recommended local workflow: use SDK control-plane helpers for writes/listing/metadata, then use direct fetch(blob.url), authenticated fetch(blob.url) for private blobs, or fetch(presignedUrl) for presigned object reads.
Writing and overwriting blobs locally
Like Vercel Blob, local writes do not overwrite existing blobs by default. Pass allowOverwrite: true when replacing an existing pathname:
await put('hello.txt', 'First version', { access: 'public' });
await put('hello.txt', 'Replacement', {
access: 'public',
allowOverwrite: true,
});Use ifMatch for optimistic concurrency when you have a current ETag.
Signed URLs locally
Use issueSignedToken() and presignUrl() against the local control plane. Local signed tokens support pathname scope, wildcard scope, operation scope, expiry, allowed content types, and maximum upload size.
Presigned private read
import { issueSignedToken, presignUrl } from '@vercel/blob';
const blob = await put('private/note.txt', 'secret', { access: 'private' });
const token = await issueSignedToken({
pathname: blob.url,
operations: ['get'],
});
const { presignedUrl } = await presignUrl(token, {
operation: 'get',
pathname: blob.url,
access: 'private',
});
const response = await fetch(presignedUrl);HEAD uses operations: ['head'] and operation: 'head'. A GET signature cannot be replayed as HEAD, or vice versa.
Presigned single-part upload
const token = await issueSignedToken({
pathname: 'uploads/file.txt',
operations: ['put'],
allowedContentTypes: ['text/plain'],
maximumSizeInBytes: 1024 * 1024,
});
const { presignedUrl } = await presignUrl(token, {
operation: 'put',
pathname: 'uploads/file.txt',
access: 'public',
allowOverwrite: true,
cacheControlMaxAge: 60,
});
const response = await fetch(presignedUrl, {
method: 'PUT',
headers: { 'content-type': 'text/plain' },
body: 'hello',
});
const blob = await response.json();Presigned PUT enforces delegation and URL-level content type and size constraints, overwrite behavior, random suffix, cache max age, and ifMatch.
Presigned multipart upload
Presigned multipart uses the control-plane /mpu route and the same put operation signature. The SDK handles this automatically for uploadPresigned(..., { multipart: true }). Manual tests can also use the SDK multipart helpers once they have a presigned payload.
import { uploadPresigned } from '@vercel/blob/client';
const blob = await uploadPresigned('videos/demo.txt', file, {
access: 'public',
handleUploadUrl: '/client-upload',
multipart: true,
});Presigned delete
const token = await issueSignedToken({
pathname: 'uploads/file.txt',
operations: ['delete'],
});
const { presignedUrl } = await presignUrl(token, {
operation: 'delete',
pathname: 'uploads/file.txt',
access: 'public',
ifMatch: currentEtag,
});
const response = await fetch(presignedUrl, { method: 'DELETE' });A missing blob returns 404; a stale ifMatch returns 412 precondition_failed; invalid signatures or scope violations return 403 forbidden.
Browser uploads and callbacks
Two browser upload flows are supported locally:
| Flow | Client helper | Server route helper |
| --- | --- | --- |
| Client-token upload | @vercel/blob/client.upload | handleUpload |
| Presigned upload | @vercel/blob/client.uploadPresigned | local presigned generation handling compatible with handleUploadPresigned request bodies |
The built-in demo route at /client-upload supports both flows for local tests. It records upload-completed callbacks at /client-upload-events.
import { uploadPresigned } from '@vercel/blob/client';
const blob = await uploadPresigned('avatar.txt', file, {
access: 'public',
handleUploadUrl: 'http://localhost:3000/client-upload',
clientPayload: JSON.stringify({ userId: '123' }),
});For presigned uploads, callback token payloads are round-tripped through the signed URL query and delivered to the local callback route. In the local demo route, the callback payload includes the pathname, client payload, and multipart flag.
Compatibility matrix
| Feature | Local status | Notes |
| --- | --- | --- |
| Public put + direct fetch | Supported | Returns .public.localhost object URL. |
| Private put + bearer fetch | Supported | Private direct reads require read-write bearer auth. |
| Object HEAD | Supported | Public unauthenticated; private bearer or presigned auth. |
| Control-plane head | Supported | SDK metadata lookup remains through VERCEL_BLOB_API_URL. |
| list, copy, createFolder, del | Supported | Existing SDK compatibility preserved. |
| Multipart SDK upload | Supported | Includes overwrite, random suffix, and ifMatch. |
| issueSignedToken() | Supported | Local read-write bearer auth; OIDC is not emulated. |
| presignUrl() GET/HEAD | Supported | Use local object URLs for best compatibility. |
| presignUrl() PUT | Supported | Includes upload constraints and callbacks. |
| Presigned multipart | Supported | Used by uploadPresigned(..., { multipart: true }). |
| Presigned DELETE | Supported | Uses DELETE /?pathname=.... |
| upload / handleUpload | Supported | Client-token upload flow. |
| uploadPresigned | Supported | Single-part and multipart tested locally. |
| Upload-completed callbacks | Supported | Client-token callbacks are HMAC-signed; local presigned callbacks are delivered to the local route for app-test inspection. |
| @vercel/blob.get() | Not supported locally | SDK-side limitation: get(url) rejects local object URLs; get(pathname) builds production object URLs instead of using VERCEL_BLOB_API_URL. Use direct fetch. |
| OIDC auth | Not supported | Use BLOB_READ_WRITE_TOKEN locally. |
| CDN/cache propagation semantics | Not supported | Local emulator only. |
Error behavior
The emulator returns Vercel Blob-style JSON errors for common cases:
400 bad_requestfor malformed requests and unsupported actions403 forbiddenfor missing/invalid bearer auth, invalid presigned signatures, expired presigned URLs, wrong operation, wrong pathname, or wrong store404 not_foundfor missing blobs412 precondition_failedfor overwrite denial and staleifMatch500 unknown_errorfor unexpected failures
Known gaps
@vercel/blob.get(...)is not supported against the local emulator because the upstream SDK validates/builds production Vercel Blob object URLs instead of routing local object reads throughVERCEL_BLOB_API_URL. Use directfetch(blob.url), authenticated privatefetch(blob.url), orfetch(presignedUrl)instead.- Real Vercel OIDC auth is not emulated; use local read-write bearer auth.
- Transparent interception of production
*.blob.vercel-storage.comhosts is not supported. - CDN behavior, propagation delays, regional storage behavior, billing semantics, and production cache invalidation are not emulated.
- Full provider policy enforcement is not implemented beyond common Vercel Blob-style JSON errors (
bad_request,not_found,precondition_failed,forbidden,unknown_error).
Local development
npm install
npm run build
npm startFor a local server on port 9966 with a named store path:
npm run serve:localDemo workflow
npm run demo