R2 uses the S3 API with AWS Signature V4 against https://<account-id>.r2.cloudflarestorage.com. MIRAGE derives this endpoint from your account ID automatically.
Credentials (R2 API token with object read/write scopes) are created the same way in both runtimes, see R2 Credentials.
Node (server-side)
Browser (presigned URLs)
The browser R2Resource is secret-free, your backend signs each operation using your R2 keys and returns a URL. R2 accepts AWS Signature V4, so @aws-sdk/s3-request-presigner works with region: 'auto' pointed at R2’s endpoint.
1. Server: sign URLs with the R2 endpoint
2. Browser: wire it up
accountId on the browser config is only used for display/logging; the actual endpoint is baked into the presigned URLs your backend returns.
R2 does accept S3-flavored PutBucketCors, but your token needs “Admin Read & Write” permissions on the bucket, object-scoped tokens will get Access Denied. Two paths:
Option A, Cloudflare Dashboard (fastest, no new token):
- Open https://dash.cloudflare.com → R2 → your bucket
- Settings tab → CORS Policy → Add CORS policy
- Origin:
http://localhost:5173 (and any production origins), methods GET,PUT,HEAD,DELETE,POST, allowed headers *
Option B, admin-scoped R2 API token + the helper script:
- Cloudflare Dashboard → R2 → Manage R2 API Tokens → Create API token
- Permissions: Admin Read & Write on your bucket
- Update
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY in .env.development
- Run the same helper used for S3:
Object-scoped tokens (Object Read & Write) cannot edit CORS. If the script returns Access Denied on R2, you’re almost certainly using an object token.
See the R2 resource docs for the equivalent Python wiring.