Object Storage
Configure Cloudflare R2 object storage to manage user-uploaded avatars and attachments
Object Storage
EasyStarter uses Cloudflare R2 as its object storage service for uploading and managing user files. Two upload types are supported out of the box:
| Upload type | Description | Size limit |
|---|---|---|
avatar | User profile picture | 5 MB |
attachment | Attachments (images, PDFs, plain text) | 25 MB |
Create an R2 Bucket
Official docs: R2 Getting started
Option 1: via Cloudflare Dashboard
- Log in to Cloudflare Dashboard
- Go to R2 Object Storage
- Click Create bucket
- Enter a bucket name, e.g.
your-app-bucket - Choose a location (Automatic is recommended)
- Click Create bucket
Option 2: via Wrangler CLI
pnpm wrangler r2 bucket create your-app-bucketConfigure wrangler.jsonc
Fill in your bucket name in the r2_buckets field of apps/server/wrangler.jsonc:
"r2_buckets": [
{
"binding": "STORAGE",
"bucket_name": "your-app-bucket"
}
],bindingmust staySTORAGE— this is the variable name used to access R2 inside the Worker. Do not change it.bucket_nameshould be the name of the bucket you created in R2.
Enable public access and get R2_PUBLIC_URL
After uploading files, you need a public URL to serve them. The recommended approach is to use R2's built-in Public Access feature.
Enable via Cloudflare Dashboard (recommended)
- Open your R2 bucket detail page
- Click the Settings tab
- Under Public Access, click Allow Access
- A public URL will be generated automatically in the format:
https://pub-xxxxxxxx.r2.dev
This URL is your R2_PUBLIC_URL.
Custom domain (optional)
You can also bind a custom domain under bucket Settings → Custom Domains, e.g. https://cdn.yourdomain.com. Use the custom domain as R2_PUBLIC_URL after binding.
Set the R2_PUBLIC_URL environment variable
R2_PUBLIC_URL must be added to two environment variable files:
Local development (apps/server/.dev.vars):
R2_PUBLIC_URL=https://pub-xxxxxxxx.r2.devProduction deployment (apps/server/.env.production):
R2_PUBLIC_URL=https://pub-xxxxxxxx.r2.dev.env.production is used to bulk-push secrets to Cloudflare Workers via pnpm run secrets:bulk:production. It does not participate in the build directly.
Configure storage parameters (optional)
Storage parameters are defined in common.storage inside packages/app-config/src/app-config.ts and can be adjusted as needed:
storage: {
provider: "r2",
publicPath: "/api/storage", // API path prefix for serving files
keyPrefixes: {
avatar: "avatars", // Key prefix for avatar files
attachment: "attachments", // Key prefix for attachment files
},
fallbackPrefix: "files", // Fallback prefix for unclassified uploads
allowedTypes: {
avatar: ["image/jpeg", "image/png", "image/gif", "image/webp"],
attachment: ["image/jpeg", "image/png", "image/gif", "image/webp",
"application/pdf", "text/plain"],
},
maxFileSizes: {
avatar: 5 * 1024 * 1024, // 5 MB
attachment: 25 * 1024 * 1024, // 25 MB
},
},Extending to other storage providers
EasyStarter's storage layer is built around the StorageProvider interface. Plugging in any storage service (e.g. AWS S3, MinIO) takes four steps.
Step 1: Register the provider key
Suppose using S3 as an example. Add the new provider key to SUPPORTED_STORAGE_PROVIDERS in packages/app-config/src/types.ts:
export const SUPPORTED_STORAGE_PROVIDERS = ["r2", "s3"] as const;Step 2: Implement the provider
Create a new file in apps/server/src/storage/providers/ that implements the StorageProvider interface:
import type { StorageProvider } from "../types";
export function createS3StorageProvider({ client, bucket }: {
client: S3Client;
bucket: string;
}): StorageProvider {
return {
async put(key, data, options) {
// Call the S3 SDK to upload
},
async get(key) {
// Call the S3 SDK to download
},
async head(key) {
// Call the S3 SDK to get metadata
},
async delete(key) {
// Call the S3 SDK to delete
},
};
}Step 3: Register it in the storage providers map
Add the new provider to the providers map in apps/server/src/storage/index.ts:
import { createS3StorageProvider } from "./providers/s3";
const providers: Record<StorageProviderKey, StorageProvider> = {
r2: createR2StorageProvider({ bucket: storage }),
s3: createS3StorageProvider({ client: s3Client, bucket: "your-bucket" }),
};Step 4: Switch the configuration
Update storage.provider in packages/app-config/src/app-config.ts to the new provider key:
storage: {
provider: "s3", // switch to the new provider
// ...other fields remain unchanged
},Once done, all file upload, download, and delete operations will automatically route through the new provider — no changes to business logic required.