ADR-001: Storage split — Cloudflare R2 (CDN media) vs Wasabi S3 (backups & archives)

Status: Accepted
Date: 2026-06-27
Deciders: Radek Konarski
Context: BrandPilot CDN architecture (issues #56–#59 in p24-infra/brandpilot)


Decision

Use two separate storage tiers based on access pattern:

TierStorageUse case
CDN / hot mediaCloudflare R2Files that are opened/served to end users
Backups / cold archivesWasabi S3Files stored long-term, rarely or never accessed

Context

BrandPilot generates and serves media assets (hero photos, avatars, icons, template previews) to potentially thousands of client landing pages worldwide. We also maintain backups for Supabase and MongoDB replica sets. These two workloads have opposite access patterns and different cost drivers.


Option comparison

Cloudflare R2

What it is: S3-compatible object storage by Cloudflare. Objects in a public R2 bucket are automatically served from Cloudflare’s global CDN — 330+ Points of Presence (PoPs) worldwide.

What is a PoP? A PoP is a physical Cloudflare data center in a specific city (Warsaw, Frankfurt, London, New York, Singapore, São Paulo, etc.). When a user anywhere in the world requests a file, Cloudflare routes them to the nearest PoP. The file is fetched from origin once, then cached at that city’s PoP — every subsequent user in that region gets it locally (5–15ms) instead of from a European server (150–250ms). A single file effectively “lives” in 330 cities simultaneously.

MetricValue
Storage$0.015/GB/month
Egress to internet$0 always
Read operations$0.36/million
Write operations$4.50/million
Free tier10 GB storage + 1M writes + 10M reads/month
CDN layerBuilt-in — no separate setup

Wasabi S3

What it is: S3-compatible object storage optimised for cold/warm data. No per-operation charges. Egress is free as long as monthly egress ≤ monthly stored volume (the 1:1 ratio policy). No built-in CDN.

MetricValue
Storage**0.007/GB/month** (6.99/TB)
Egress to internet0 if egress ≤ stored volume; 0.01/GB above ratio
Operations$0 — no per-request charges
Minimum object duration90 days (charged even if deleted earlier)
CDN layerNone

Cost comparison by workload

Workload A — CDN media (BrandPilot: photos, avatars, previews)

Assumption: 1,000 brands × 5 assets × 1.5 MB = 7.5 GB stored. Monthly traffic: 50,000 page views × 800 KB media/page = 40 GB egress.

WasabiCloudflare R2
Storage$0.05$0.11
Egress 40 GB$0.40 (exceeds 7.5 GB stored ratio)$0
CDN setupExtra work (Caddy proxy or separate CDN)Included
Total~$0.45 + CDN overhead~$0.11

At 10,000 brands / 1M page views / month: Wasabi ~8/month vs R2 ~1–6/month. Gap grows with traffic. R2 wins for CDN media.

Workload B — Backups & archives (Supabase, MongoDB rs0, long-term)

Assumption: 2 TB stored. Monthly restore/access: ~10 GB (rare — only on incident).

WasabiCloudflare R2
Storage 2 TB$14/month$30/month
Egress 10 GB$0 (10 GB << 2 TB stored)$0
Operations$0~$0 (few ops on cold data)
Total~$14/month~$30/month

Wasabi is ~2× cheaper for cold storage. Egress is irrelevant at low access ratios. No CDN needed for backups. Wasabi wins for archives.


Decision rationale

  • R2’s zero-egress advantage only matters when files are served frequently to end users. For backups that are accessed <1× per incident, egress cost is negligible on Wasabi.
  • Wasabi’s storage rate (0.007/GB) is 2× cheaper than R2 (0.015/GB) — this compounds at multi-TB backup scales.
  • R2’s built-in CDN (330+ PoPs) eliminates additional infrastructure for media delivery. Adding a CDN layer to Wasabi would cost extra and add operational complexity.
  • Both are S3-compatible — the same SDK (@aws-sdk/client-s3) works with both, just different endpoint URLs and credentials.

Scope of this decision

Cloudflare R2 — use for:

  • BrandPilot media: hero backgrounds, section photos, HeyGen-generated avatars, template icons, preview screenshots
  • Any file that is served to end users via a public URL (landing pages, brand kits, portfolio assets)
  • Files requiring global low-latency delivery

Wasabi S3 — use for:

  • Supabase database backups — daily encrypted dumps, retained 30 days
  • MongoDB rs0 backups — replica set dumps from bms-2/bms-3, retained 30 days
  • Thanos long-term metrics — Prometheus block uploads from vps-i1, retained 1 year
  • Application log archives — compressed log exports, retained 90 days
  • PDF report archives — generated inspection reports from report-scheduler, retained indefinitely
  • Any file stored for compliance/recovery purposes with rare or no routine access

Existing Wasabi bucket — no migration

p24-infra bucket on Wasabi (eu-central-2) remains as-is for all existing monitoring, backup, and Thanos workloads. No migration needed.


Implementation

R2 (new — BrandPilot)

  • Bucket: p24-brandpilot-media
  • Public URL: media.brandpilot.io (CNAME to R2 public endpoint)
  • Credentials: CLOUDFLARE_R2_ACCOUNT_ID, CLOUDFLARE_R2_ACCESS_KEY_ID, CLOUDFLARE_R2_SECRET_ACCESS_KEY in secrets/brandpilot.env.sops
  • SDK: @aws-sdk/client-s3 with endpoint: https://{account_id}.r2.cloudflarestorage.com
  • See: p24-infra/brandpilot issue #59

Wasabi (existing — unchanged)

  • Bucket: p24-infra (eu-central-2)
  • Credentials: P24_INFRA_WASABI_ACCESS_KEY / P24_INFRA_WASABI_SECRET_ACCESS_KEY in secrets/monitoring.env.sops
  • Covers: Thanos, backups, PDF archives, log exports

Consequences

  • Positive: Lower cost for both workloads; media delivery is globally fast without extra infrastructure; backups stay cheap at multi-TB scale.
  • Negative: Two storage providers to manage, two credential sets to rotate.
  • Neutral: Both are S3-compatible — code reuse is high; only endpoint URL and credentials differ.
  • Future trigger to revisit: If Cloudflare R2 drops storage pricing below Wasabi, consolidate everything to R2. Currently R2 storage is 2× more expensive.