Skip to content

Setup R2

github-actions[bot] edited this page Oct 14, 2026 · 1 revision

T-0.3 — Cloudflare R2 bucket and credentials

Blocks Phase 2 deployment only. Local development and CI are unblocked by the MinIO service in docker-compose.yml: aws-sdk-go-v2/s3 targets both and only the endpoint and keys differ. So this is not urgent — but it is worth doing before T-2.3 writes the upload path, so the first real upload is not also the first time anyone touches R2.

Owner: Thuong. Verify with make verify-r2 when done.


Console steps

https://dash.cloudflare.com → R2

1. Create the bucket

  • Name: quizzivy-media (§4 fixes this name).
  • Location hint: Asia-Pacific, closest to the students.
  • Storage class: Standard.

2. Leave it private — this is the one that matters

§11.2 requires a private bucket: no public listing, no public read. After creating it, open Settings and confirm:

  • Public Development URL (r2.dev): disabled. Cloudflare offers a one-click public subdomain. Enabling it makes every object world-readable by URL, which would hand out every listening file to anyone who shares a link.
  • Custom domains: none.

Audio is served through short-lived signed URLs, 10 minutes, minted per request (§11.2). make verify-r2 asserts that an unsigned request is refused, so if this gets flipped on later, the check fails.

Worth repeating §11.2's honest note: signed URLs raise the cost of copying a file slightly. They do not prevent it. controlsList="nodownload" prevents nothing at all. If a listening file must never leak, it should not be online.

3. Create a scoped API token

R2 → API → Manage API Tokens → Create API Token.

  • Permission: Object Read & Write — not Admin. The app uploads and reads objects; it never needs to create or delete buckets.
  • Specify bucket: Apply to specific buckets → quizzivy-media. A token scoped to one bucket cannot touch anything else in the account.
  • TTL: your call. A non-expiring token is one less outage; a rotating one is tighter. At this scale either is defensible.

Cloudflare shows the Access Key ID and Secret Access Key once. The secret is not retrievable afterwards — if it is lost, create a new token.

4. Put the values in .env

R2_ACCOUNT_ID=<32-hex account id>
R2_ACCESS_KEY_ID=<access key id>
R2_SECRET_ACCESS_KEY=<secret access key>

# What the SERVER reads. The R2_* names above are for `make verify-r2` only.
S3_ENDPOINT=https://<R2_ACCOUNT_ID>.r2.cloudflarestorage.com
S3_BUCKET=quizzivy-media
S3_ACCESS_KEY_ID=<access key id>
S3_SECRET_ACCESS_KEY=<secret access key>
S3_REGION=auto

The two sets of names are not interchangeable. verify-r2.sh derives the endpoint from R2_ACCOUNT_ID; the server does not, and reads S3_ENDPOINT directly. An earlier version of this page said "the endpoint is derived, not configured", which was true only of the script — following it left the server with credentials, a bucket, and no endpoint, which it used to treat as configured and then resolve against real AWS S3 rather than R2. It now refuses to start instead, naming the missing variable.

Do not set S3_FORCE_PATH_STYLE for R2. It defaults to false, which is what R2 wants: R2 serves buckets as a subdomain. The variable exists for local MinIO, which serves them as a path.

Region must be auto. Cloudflare aliases an empty value and us-east-1 to auto for tools that cannot express it, but set it explicitly.

.env is gitignored. Do not paste the secret into a commit, an issue, or a chat.

5. Verify

make verify-r2

Runs against real R2 using the pinned mc source build from the compose stack, so there is nothing extra to install. It checks:

  1. the credentials are accepted and the bucket is reachable
  2. an object can be written
  3. it can be read back
  4. a presigned GET returns 200 — the §11.2 delivery path
  5. an unsigned GET is refused (401/403) — the bucket is genuinely private
  6. the test object is cleaned up

Note for whoever implements T-2.3

R2's S3 compatibility has one sharp edge worth knowing before debugging it live.

Recent aws-sdk-go-v2 versions compute and send a checksum header on PutObject by default. R2 implements only a limited set of checksum algorithms — CRC64NVME for full-object, and CRC-32/CRC-32C/SHA-1/SHA-256 for composite — and reports most x-amz-sdk-checksum-algorithm / x-amz-checksum-* headers as unimplemented. The result is an upload that works perfectly against MinIO in development and fails against R2 in production.

Configure the client so checksums are only sent when required:

cfg.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
cfg.ResponseChecksumValidation  = aws.ResponseChecksumValidationWhenRequired

Also relevant, from Cloudflare's compatibility matrix: R2 does not implement ACLs, bucket policies, tagging, versioning, object locking, or notifications. None of those are used by this design — object immutability (§11.1) is enforced by never reusing a key, not by S3 versioning — but do not reach for them.

T-2.3's acceptance should include one upload against real R2, not only MinIO. The two behave identically right up until they do not.

The Word import bucket

Word import uses a second, private bucket (quizzivy-imports). It is reached with the same token, so the token must be scoped to both buckets.

Its artifacts are written create-only and must be checksum-verified. Cloudflare's S3 matrix marks full-object SHA-256 checksums unsupported, so storage.PutImmutable does not send x-amz-checksum-sha256. Instead it does three things:

  • It checks the size and SHA-256 itself before sending anything.
  • It sends Content-MD5 for transport integrity and If-None-Match: * so nothing is overwritten.
  • It stores the digest as x-amz-meta-sha256, which a lost-response retry compares.

The worker verifies SHA-256 again on every read.

make verify-r2-imports runs that code against the real bucket (R2_IMPORT_BUCKET, default quizzivy-imports). Run it after creating the bucket or changing the token. It cannot see r2.dev or custom-domain exposure. Check those with wrangler r2 bucket dev-url get and wrangler r2 bucket domain list.


Generated from docs/setup/r2.md. Edit that file in the repository; changes made on this page are overwritten.

Home

Start here

Product

Implementation plan

Redesign (Phase R)

Setup and operations

Design

Team

Code READMEs

Clone this wiki locally