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:
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:
-
Install whatever SDK your backend needs.
-
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() } -
Wire it into the
selectBlobStore()switch inselect.tsbehind a newBLOB_STOREvalue.
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:
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.