Storage
OpenCauldron needs a place to store reference images you upload and the media files it generates. Two backends are available: a local filesystem backend for development and a Cloudflare R2 backend for production. The backend is selected at startup via a single environment variable.
Choosing a backend
Section titled “Choosing a backend”Set STORAGE_PROVIDER in your .env file:
# Local filesystem — easy for development, not accessible to external APIsSTORAGE_PROVIDER="local"
# Cloudflare R2 — required for production and for image-to-video generationSTORAGE_PROVIDER="r2"If STORAGE_PROVIDER is not set, the app defaults to r2.
Local filesystem
Section titled “Local filesystem”The local backend writes files to an uploads/ directory at the project root and serves them through the built-in /api/uploads/ route. No credentials are required.
This is the right choice when you are running OpenCauldron locally and only generating images. It is not suitable for production and it will not work for image-to-video generation (see below).
Why local storage breaks image-to-video
Image-to-video models (Veo 3, Runway, Kling, Hailuo, Ray) require a publicly accessible URL for the reference image — the AI provider’s API fetches the image directly from that URL. Local storage URLs are relative paths like /api/uploads/... that are only reachable inside your own machine. External APIs cannot reach them.
If you want to use image-to-video, you must use a storage backend that returns a public URL. Cloudflare R2 with a public bucket (or any other publicly accessible object storage) satisfies this requirement.
Cloudflare R2
Section titled “Cloudflare R2”The R2 backend uses the AWS S3-compatible API. Configure it with five environment variables:
STORAGE_PROVIDER="r2"R2_ACCOUNT_ID="your-cloudflare-account-id"R2_ACCESS_KEY_ID="your-r2-access-key-id"R2_SECRET_ACCESS_KEY="your-r2-secret-access-key"R2_BUCKET_NAME="cauldron"R2_PUBLIC_URL="https://your-bucket.your-domain.com" # optional but strongly recommendedR2_PUBLIC_URL is optional but strongly recommended. When set, every uploaded file gets a stable public URL at {R2_PUBLIC_URL}/{key}. When omitted, the backend falls back to presigned URLs that expire after one hour — this is fine for viewing but breaks any workflow that stores the URL for later use (such as referencing a generated image when creating a video).
To set up R2:
- Go to Cloudflare Dashboard > R2 Object Storage and create a bucket.
- Under Manage R2 API Tokens, create a token with read and write permissions on the bucket. Copy the Account ID, Access Key ID, and Secret Access Key.
- To enable public access, connect a custom domain to the bucket under the bucket’s Settings > Custom Domains tab. Use that domain as
R2_PUBLIC_URL.
Upload limits and allowed types
Section titled “Upload limits and allowed types”The upload endpoint at POST /api/uploads enforces the following limits:
| Constraint | Value |
|---|---|
| Maximum file size | 10 MB |
| Allowed types | PNG, JPEG, WebP, GIF |
Requests that exceed the size limit or use an unsupported type are rejected with a 400 response before any storage operation occurs.
Key naming
Section titled “Key naming”Files are stored under two key prefixes:
| Prefix | Used for |
|---|---|
user-uploads/{userId}/{timestamp}-{nanoid}.{ext} | Reference images uploaded by users via the upload endpoint |
assets/{userId}/{timestamp}-{nanoid}.{ext} | Generated images and videos produced by the app |
Thumbnails are stored alongside their originals with a _thumb.webp suffix — for example, assets/{userId}/1234567890-abcde12345.png gets a thumbnail at assets/{userId}/1234567890-abcde12345_thumb.webp.
Thumbnail generation
Section titled “Thumbnail generation”Every image uploaded or generated by the app gets a thumbnail automatically. Thumbnails are:
- Resized to 400px wide (height scaled proportionally)
- Encoded as WebP at 80% quality
- Stored alongside the original file under the same key prefix
Thumbnails are generated using sharp. No additional configuration is required — thumbnail generation is always on.
Bring your own storage
Section titled “Bring your own storage”The storage system is built around a three-method interface:
export interface StorageBackend { upload(buffer: Buffer, key: string, contentType: string): Promise<string>; getUrl(key: string): Promise<string>; delete(key: string): Promise<void>;}upload— stores a file buffer at the given key and returns the file’s public URLgetUrl— returns the URL for an already-stored key (may be a presigned URL if the backend requires auth)delete— removes the file at the given key
To add a new backend (Amazon S3, Google Cloud Storage, Azure Blob Storage, or any other object store):
- Create a new file in
src/lib/storage/, e.g.src/lib/storage/s3.ts. - Implement the
StorageBackendinterface and export your backend instance. - In
src/lib/storage/index.ts, add a condition ingetBackend()to return your implementation whenSTORAGE_PROVIDERmatches your chosen identifier.
The rest of the application — uploads, asset generation, thumbnails, URL retrieval — will use your backend automatically.