Skip to content

ADR 0013: Cloudflare R2 (prod) + MinIO (local) for Object Storage

ADR 0013: Cloudflare R2 (prod) + MinIO (local) for Object Storage

Section titled “ADR 0013: Cloudflare R2 (prod) + MinIO (local) for Object Storage”
  • Status: Accepted
  • Date: 2026-04-14
  • Deciders: Ideony team

Ideony stores user-uploaded files: professional credential documents (P_IVA, insurance certificates, diplomas), profile photos, and job-site images. Storage must be S3-compatible (presigned URL flow), cost-effective, and accessible from the NestJS API via @aws-sdk/client-s3.

Use Cloudflare R2 in production (S3-compatible, zero egress fees) and MinIO in local development (Docker service on port 9000/9001, also S3-compatible). Both are accessed via @aws-sdk/client-s3 with identical code paths; only the endpoint URL and credentials change per environment.

  • Zero egress fees on R2 — unlike S3/GCS, downloading files to the API or serving to clients costs nothing beyond storage.
  • S3-compatible API means the same StorageService wrapper works for both R2 (prod) and MinIO (local) without conditional branches.
  • MinIO local instance (pnpm docker:up) enables offline development and CI without touching prod storage.
  • R2 EU region (eu.r2.cloudflarestorage.com) keeps object data in Europe for GDPR.
  • Presigned URLs (600 s TTL for credential uploads, rate-limited 10/day per professional via Redis) mean the API never proxies file bytes.
  • R2 public bucket access requires a Cloudflare Workers or R2.dev subdomain; custom domain needs Workers route or R2 public bucket toggle.
  • MinIO Docker image adds ~200 MB to local dev stack; teams on slow machines notice startup time.
  • R2 token scoping must be reviewed: overly broad tokens are a security risk if the API is compromised.
  • AWS S3 — rejected: egress fees add up quickly for media-heavy credential uploads; adds AWS billing account overhead alongside R2.
  • Supabase Storage — rejected: ties storage to a secondary Postgres provider; adds unnecessary coupling.
  • Self-hosted Minio on CAX11 — rejected: CAX11 disk (40 GB) insufficient for production media; no CDN or geo-redundancy.