npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

local-blobtastic

v0.2.0

Published

Local development emulator for the Vercel Blob API.

Downloads

332

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 with fetch(blob.downloadUrl)
  • direct private blob GET, HEAD, and downloadUrl reads with Authorization: Bearer <BLOB_READ_WRITE_TOKEN>
  • presigned private object GET and HEAD
  • control-plane head
  • put, including default no-overwrite behavior, allowOverwrite, addRandomSuffix, ifMatch, cache max age, and Vercel Blob-style downloadUrl
  • presigned single-part PUT, including allowed content type, maximum size, overwrite, random suffix, cache max age, and ifMatch constraints
  • multipart put via { multipart: true }, with the same overwrite, suffix, and ifMatch behavior as regular put
  • presigned multipart create, part upload, and complete
  • copy, including allowOverwrite, addRandomSuffix, ifMatch, and copy-request metadata semantics
  • createFolder
  • del, including single-URL ifMatch
  • presigned DELETE /?pathname=..., including vercel-blob-if-match
  • list, including cursor pagination and mode: 'folded'
  • issueSignedToken() and SDK-compatible local delegation/client signing tokens
  • presignUrl() URLs for local get, head, put, multipart put, and delete workflows
  • client-token uploads via @vercel/blob/client.upload
  • client-token multipart uploads
  • presigned browser uploads via @vercel/blob/client.uploadPresigned
  • upload-completed callbacks through handleUpload and local presigned callback handling
  • Vercel Blob-style JSON errors for common SDK error mapping

Run

From npm once published:

npx local-blobtastic

With options:

npx local-blobtastic --port 9966 --store .local-blob-store

Options:

-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:3000

Keep 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.png
  • http://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_request for malformed requests and unsupported actions
  • 403 forbidden for missing/invalid bearer auth, invalid presigned signatures, expired presigned URLs, wrong operation, wrong pathname, or wrong store
  • 404 not_found for missing blobs
  • 412 precondition_failed for overwrite denial and stale ifMatch
  • 500 unknown_error for 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 through VERCEL_BLOB_API_URL. Use direct fetch(blob.url), authenticated private fetch(blob.url), or fetch(presignedUrl) instead.
  • Real Vercel OIDC auth is not emulated; use local read-write bearer auth.
  • Transparent interception of production *.blob.vercel-storage.com hosts 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 start

For a local server on port 9966 with a named store path:

npm run serve:local

Demo workflow

npm run demo