EasyStarter logoEasyStarter

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 typeDescriptionSize limit
avatarUser profile picture5 MB
attachmentAttachments (images, PDFs, plain text)25 MB

Create an R2 Bucket

Official docs: R2 Getting started

Option 1: via Cloudflare Dashboard

  1. Log in to Cloudflare Dashboard
  2. Go to R2 Object Storage
  3. Click Create bucket
  4. Enter a bucket name, e.g. your-app-bucket
  5. Choose a location (Automatic is recommended)
  6. Click Create bucket

Option 2: via Wrangler CLI

pnpm wrangler r2 bucket create your-app-bucket

Configure wrangler.jsonc

Fill in your bucket name in the r2_buckets field of apps/server/wrangler.jsonc:

apps/server/wrangler.jsonc
"r2_buckets": [
  {
    "binding": "STORAGE",
    "bucket_name": "your-app-bucket"
  }
],
  • binding must stay STORAGE — this is the variable name used to access R2 inside the Worker. Do not change it.
  • bucket_name should 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)

  1. Open your R2 bucket detail page
  2. Click the Settings tab
  3. Under Public Access, click Allow Access
  4. 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):

apps/server/.dev.vars
R2_PUBLIC_URL=https://pub-xxxxxxxx.r2.dev

Production deployment (apps/server/.env.production):

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:

packages/app-config/src/app-config.ts
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:

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:

apps/server/src/storage/providers/s3.ts
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:

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:

packages/app-config/src/app-config.ts
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.