Skip to content

Private vs public Vercel Blob stores: picking one you cannot change

A Blob store's access mode is fixed at creation. Private needs a credential for every read; public is readable by anyone with the URL. How to serve each, and why user files belong in private.

Vercel Blob2 min readships at docs/solutions/vercel-blob/private-vs-public-blob-stores.md

Tags: vercel-blob · private-storage · public-storage · signed-urls · security

When you create a Vercel Blob store you pick Private or Public. You cannot change it later. Pick wrong and you migrate every file to a new store.

Private stores are generally available and need @vercel/blob 2.3 or newer.

The difference

PrivatePublic
WriteAuthenticatedAuthenticated
ReadNeeds a credentialAnyone with the URL
URL host<store>.private.blob.vercel-storage.com<store>.public.blob.vercel-storage.com
Search indexingImpossiblePossible

Every put(), upload() and get() names the mode with access. It must match the store.

Public: the pathname is the password

A public blob URL never expires. Anyone who gets it can read the file, forever, from anywhere. A random suffix makes URLs hard to guess. It does not make them secret once shared: logs, referrer headers, a screenshot, a support chat.

Right for: marketing images, public avatars, anything you would put on a CDN anyway.

Private: three ways to serve a file

  1. Presigned GET URL. Your server calls issueSignedToken() once, then presignUrl() per file with a short validUntil. The browser reads straight from the store. The URL dies on schedule.

    import { issueSignedToken, presignUrl } from "@vercel/blob";
    
    const token = await issueSignedToken({ pathname: "*", operations: ["get"] });
    const { presignedUrl } = await presignUrl(token, {
      operation: "get",
      pathname: key,
      access: "private",
      validUntil: Date.now() + 15 * 60 * 1000,
    });
    

    issueSignedToken() is a network call; presignUrl() is a local HMAC. Cache the token on the server until near its expiry (it defaults to one hour, seven days at most) and sign many URLs with it. Keep the token's clientSigningToken on the server: anyone holding it can sign URLs.

  2. Stream through a route. Check auth, get() the blob, return its stream. You control every header, including Content-Disposition for a custom download name. You also pay transfer twice and hold a function open for the whole download. Set Cache-Control: private, no-cache (or no-store for sensitive data), and never cache the response on the CDN with s-maxage.

  3. BLOB_READ_WRITE_TOKEN as a bearer header. For server-to-server only. It is a full read-write credential.

Which to pick

  • Anything a user uploaded: private. A leaked link expires.
  • Anything public by nature: public. Cheaper to serve, no signing.
  • Both? Two stores. Hobby allows 100, Pro 500. There is no per-blob access setting to mix them.