Skip to content

CORS on Cloudflare R2

This guide covers the manual CORS setup when Relyn’s auto-attempt didn’t go through. Takes about a minute in the Cloudflare dashboard.

  1. Open the bucket in Cloudflare R2. In the Cloudflare dashboard, go to R2Overview, click the bucket you just connected to Relyn, then Settings in the top tab bar.

  2. Scroll to “CORS Policy” and click Add CORS policy (or Edit if one already exists).

  3. Paste the JSON policy. Two versions, depending on whether you want the minimum-to-test or production-ready setup.

    Minimal (just enough to test from local dev)

    [
    {
    "AllowedOrigins": ["http://localhost:5173"],
    "AllowedMethods": ["GET"]
    }
    ]

    Production-ready (recommended) — replace https://studio.relyn.app with your studio URL if you self-host:

    [
    {
    "AllowedOrigins": [
    "https://studio.relyn.app",
    "http://localhost:5173"
    ],
    "AllowedMethods": ["GET", "PUT", "HEAD", "POST", "DELETE"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
    }
    ]
  4. Save. CORS changes propagate within a few seconds.

  5. Back in Relyn, open the bucket and click Diagnose — the yellow warning will clear once the preflight check confirms the policy works.

  • AllowedOrigins — exact web origins the browser is allowed to call from. No wildcards needed for Relyn.
  • AllowedMethodsGET, PUT, HEAD cover normal uploads + reads. POST and DELETE are for multipart uploads and bulk-delete from the gallery.
  • AllowedHeaders: [”*”] — Relyn sends a few custom headers in presigned PUTs (content-type, x-amz-*). Wildcard is safe because the upload URL is short-lived and scoped to one object.
  • ExposeHeaders: [“ETag”] — required so multipart uploads can read the part ETag back. Single-part uploads don’t strictly need it; exposing ETag is harmless.
  • MaxAgeSeconds: 3600 — how long the browser caches the CORS preflight. One hour is comfortable.

The auto-attempt fires once, on bucket connect, when you’ve just provided fresh credentials. That’s the only moment the request makes sense — you’re actively setting the bucket up and we have your explicit attention.

After that, the bucket belongs to you again. Relyn doesn’t keep retrying PutBucketCors in the background because the keys you give us may legitimately be scoped to object operations only (Get / Put / Delete on objects), without PutBucketCors on the bucket itself. That’s a sensible permission posture and we don’t want a UI that pretends otherwise.