Skip to main content
In development. 86d is being built in the open. Every capability is Experimental until it earns evidence, so check maturity levels before you rely on anything here.
Product images, PDFs, and other media all go through one storage interface. Pick where they land with STORAGE_CLIENT. Object keys carry the owning Store’s scope, so files from two Stores cannot collide, and the authenticated routes still do the access control. Get this wrong on a stateless host and your Product images vanish on the next deploy. It is worth two minutes.

Which one to use

Local

Local storage writes to a directory on the same filesystem as the app. It is the default under docker compose up and needs nothing external.
.env
The Store serves those files at GET /uploads/[...path] with immutable cache headers. SVG files get a restrictive Content Security Policy (CSP) of default-src 'none'; style-src 'unsafe-inline', because an SVG is a document that can carry script. PDFs are served as attachments rather than rendered inline.
On Vercel and other stateless hosts, local storage is wiped on every deploy. Use vercel or s3 there.

Vercel Blob

On Vercel, attach a Blob store to your project. Vercel injects BLOB_READ_WRITE_TOKEN for you.
.env
Do not set BLOB_READ_WRITE_TOKEN by hand. Linking a Blob store in the Vercel dashboard adds it to your deployment environment.

S3-compatible

The s3 provider covers AWS S3, Cloudflare R2, MinIO, and Railway. Other S3-compatible endpoints may work. Upload a file, reload the page, and confirm the image renders before you trust one.
.env
The bundled docker-compose.yml already runs MinIO:
.env
Set STORAGE_PUBLIC_URL_MODE=proxy so upload URLs point at the Store at /uploads/.... The MinIO container hostname does not resolve in a browser.
R2 speaks the S3 API. Your endpoint is in the R2 dashboard under bucket settings.
.env
Railway uses virtual-hosted-style URLs, so add S3_VIRTUAL_HOSTED_STYLE=true to the standard S3 variables.
.env

Serving from your own domain

STORAGE_PUBLIC_URL_MODE decides what URL the upload API hands back. It applies to local and s3. Reach for proxy when MinIO is inside Docker Compose and the browser cannot reach the bucket, when you want every image served from your own domain, or when the bucket is private and access control belongs in the application.
.env

Limits and accepted types

Every upload goes through POST /api/upload, which is admin-only. Files are validated against their magic bytes, so a fake Content-Type header changes nothing. SVGs are additionally scanned for embedded scripts, event handlers, and javascript: URIs, and a file that fails returns 400.

Where files land

Uploads are stored at stores/{storeId}/{uuid} under the configured storage root. That storeId segment is what keeps one Store from reading or deleting another Store’s files.