Skip to Content

Blob store

Canvas thumbnails are assembled from per-frame screenshots, which are captured by a headless browser (or the desktop webview), resized, and uploaded to a publicly readable blob store. The default backend is @vercel/blob via apps/app/lib/blob/vercel.ts, fronted by a thin re-export in apps/app/lib/blob/index.ts that exposes a backend-agnostic BlobStore interface. Any object store with a public-URL read path works — S3, R2, GCS, Supabase Storage, a self-hosted MinIO bucket, etc.

Selecting a backend

apps/app/lib/blob/index.ts resolves the store through selectBlobStore() in apps/app/lib/blob/select.ts, which reads the BLOB_STORE env var:

BLOB_STOREBackend
unset / vercelVercel Blob (vercel.ts) — the hosted default
local-fsLocal-filesystem store (local-fs.ts) — desktop build

Only the explicit value local-fs opts into the local store; anything unrecognised stays on Vercel Blob, so a typo never silently swaps the backend.

The local-fs store writes blobs to a directory (LOCAL_BLOB_DIR) and returns the localhost URL that directory is served from (LOCAL_BLOB_BASE_URL), so the desktop build can produce per-canvas thumbnails with no cloud blob store.

Using a different blob backend

The exported blobStore is typed as the backend-agnostic BlobStore interface defined in apps/app/lib/blob/types.ts, so any implementation of that interface is a drop-in replacement. To add one:

  1. Install whatever SDK your backend needs.

  2. Add a sibling factory — e.g. apps/app/lib/blob/s3.ts:

    import "server-only" import type { BlobStore } from "./types" class S3BlobStore implements BlobStore { async put(key, body, opts) { /* call your SDK, return { url } */ } } export function getS3BlobStore(): BlobStore { return new S3BlobStore() }
  3. Wire it into the selectBlobStore() switch in select.ts behind a new BLOB_STORE value.

The BlobStore interface is intentionally tiny (put(key, body, opts) → { url }) — see apps/app/lib/blob/types.ts for the exact shape. Callers (lib/thumbnail/) only ever see the abstract interface and need no changes when the backend swaps.

The default backend reads its credentials from PUBLIC_BLOB_READ_WRITE_TOKEN — on Vercel, connect a public Blob store to your project with the env var prefix PUBLIC_BLOB and the token is injected automatically; locally, run vercel env pull .env.local after connecting the store. A different backend reads whatever env vars you wire into its factory.

File store

Canvas files, the files agents save for a canvas’s members, are private: only members of the canvas can read them, through /api/canvas-files/<canvas>/<path>, which checks membership. Their bytes go to a separate file store (apps/app/lib/files) with no public URLs, picked by the same BLOB_STORE value:

BLOB_STOREFile store
unset / vercelA private Vercel Blob store, PRIVATE_BLOB_READ_WRITE_TOKEN
local-fsA local directory, LOCAL_FILES_DIR, that nothing serves directly

A Vercel Blob store’s access is set when it’s created, and the thumbnails store is public, so files need a second store. In the Vercel dashboard, create a Blob store with private access and connect it to the project with the env var prefix PRIVATE_BLOB; PRIVATE_BLOB_READ_WRITE_TOKEN is then injected. Without it, agents can’t save or open files, and say so when asked to.

Files people attach to chat messages land in the same store. On a Vercel Blob store, an attachment over 4 MB uploads from the browser straight to the store (a function takes a body of at most 4.5 MB) with a token the app hands out for that canvas only; the app then checks its size and type and adds it to the canvas’s files.

A different backend implements the FileStore interface in apps/app/lib/files/store.ts (put, get, delete and size by key) and is wired into apps/app/lib/files/index.ts.

Last updated on