Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0007: Cloudflare R2 for file storage; separate compliance bucket with object-lock

ADR-0007: Cloudflare R2 for file storage; separate compliance bucket with object-lock

Status: Accepted Date: ~2026-03 (estimate, around FIT-158 compliance forms work) Context owner: Owner

Context

FitKit stores three classes of files:

  1. General assets — CSV exports, member avatars, attached photos in messages.
  2. Progress photos — sensitive (member body images); access strictly scoped per member + their coach.
  3. Compliance artifacts — signed waivers, health declarations, parental consents, studio rental contracts. Israeli Sports Promotion Law requires 7+ years retention, immutability (no edits or deletions), and provability of who signed when.

Different durability and access requirements per class. Mixing them in one bucket is a foot-gun (one wrong lifecycle policy and you’ve deleted a court-relevant document).

Decision

Use Cloudflare R2 as the object store. Two buckets:

  • R2_BUCKET_NAME (e.g., fitkit-exports) — general assets. Standard lifecycle, deletable.
  • R2_COMPLIANCE_BUCKET_NAME (e.g., fitkit-compliance) — signed PDFs and other regulatory artifacts. Bucket-level object-lock + lifecycle retention configured before production. No deletion permitted.

Access is via presigned URLs minted by the API:

  • Standard presign: authenticated user via Clerk; URL embeds a short TTL and a scope to the user/org.
  • Public token-gated presign (FIT-189): lets a member receive a signing link on WhatsApp/SMS and upload their signature without being signed in. The token is short-lived, single-use, and scoped to a specific form instance.

In dev, R2_COMPLIANCE_BUCKET_NAME falls back to R2_BUCKET_NAME for convenience. In production, the two MUST be different buckets, and the compliance bucket MUST have object-lock enabled at the provider level before any real data lands.

Consequences

Positive

  • Compliance is a property of the bucket, not application code. We can’t accidentally delete a signed PDF by writing the wrong endpoint.
  • R2 has no egress fees — the web app can stream large files without us paying twice.
  • API stays stateless; files don’t touch local disk.
  • The presign model means the API doesn’t proxy file uploads (good for throughput).

Negative

  • Two-bucket architecture is one more thing to remember when adding a new file flow. Mistakes default to the general bucket, which is less safe for new compliance artifacts.
  • Cloudflare R2 has had occasional regional latency spikes. We don’t multi-region today.
  • Audit on the bucket itself (who downloaded what when) is limited to Cloudflare’s logs — not yet integrated with our audit pipeline (also a gap; see FIT-20).

Discipline

  • New feature that writes a regulatory artifact? Use the compliance bucket and document the retention requirement in the feature’s behavior.md.
  • Never hardcode the bucket name — always go through env config.
  • For dev, set PUPPETEER_EXECUTABLE_PATH so the forms PDF renderer can run locally.