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
Context
Section titled “Context”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.
Decision
Section titled “Decision”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.
Consequences
Section titled “Consequences”- 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
StorageServicewrapper 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.
Alternatives considered
Section titled “Alternatives considered”- 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.