Cloudflare R2
cloudflare-r2Storage34 lines
S3-compatible object storage with no egress fees, accessed through presigned URLs.
Current — as published in v1.3.2. Written by hand for Cloudflare R2, not generated.
The rule
Credentials
- Server-side only. R2 keys are account-scoped; there is no client-safe variant. Never in the app, never in an
EXPO_PUBLIC_*variable. - Scope API tokens to a single bucket with least privilege.
Presigned URLs
- The app never talks to R2 directly. It asks the backend for a presigned URL.
- The backend chooses the object key, namespaced by owner:
{userId}/{generatedId}. A client-supplied key lets one user overwrite another's file. - Validate content type and size *before* signing. Once issued, the URL is a capability anyone holding it can use.
- Shortest workable
expiresIn.
Storage
- Persist the object key, never a presigned URL — it expires.
region: 'auto'; R2 ignores regions but the SDK requires the field.
Buckets
- Private by default. Serve public assets through a custom domain with caching rather than enabling public bucket access.
- Add a lifecycle rule expiring incomplete multipart uploads, or you are billed for objects that never appear in a listing.
Never
- Never let a client determine an object key.
- Never log a secret access key or a presigned URL.
As each tool receives it
The same guidance, in the shape each editor reads. Cursor and Claude Code are written separately by hand; the rest are derived from the Cursor rule.
.cursor/rules/cloudflare-r2.mdc
---
description: Cloudflare R2 conventions
globs: ["server/**", "api/**", "src/services/storage/**"]
alwaysApply: false
---
# Cloudflare R2
## Credentials
- **Server-side only.** R2 keys are account-scoped; there is no client-safe
variant. Never in the app, never in an `EXPO_PUBLIC_*` variable.
- Scope API tokens to a single bucket with least privilege.
## Presigned URLs
- The app never talks to R2 directly. It asks the backend for a presigned URL.
- **The backend chooses the object key**, namespaced by owner:
`{userId}/{generatedId}`. A client-supplied key lets one user overwrite
another's file.
- Validate content type and size *before* signing. Once issued, the URL is a
capability anyone holding it can use.
- Shortest workable `expiresIn`.
## Storage
- Persist the object **key**, never a presigned URL — it expires.
- `region: 'auto'`; R2 ignores regions but the SDK requires the field.
## Buckets
- Private by default. Serve public assets through a custom domain with caching
rather than enabling public bucket access.
- Add a lifecycle rule expiring incomplete multipart uploads, or you are billed
for objects that never appear in a listing.
## Never
- Never let a client determine an object key.
- Never log a secret access key or a presigned URL.
What else this module writes
Selecting Cloudflare R2 contributes more than a rule file — all of it merged with every other module you pick, with conflicts resolved rather than duplicated.
Environment
R2_ACCOUNT_IDrequiredCloudflare account id, used to build the S3 endpoint.R2_ACCESS_KEY_IDrequiredAPI token access key. Server-side only.R2_SECRET_ACCESS_KEYrequiredAPI token secret. Shown once at creation. Server-side only.R2_BUCKETrequiredBucket name.R2_PUBLIC_BASE_URLoptionalCustom domain for genuinely public assets. Preferred over enabling public bucket access.Dependencies
@aws-sdk/client-s3^3.1102.0@aws-sdk/s3-request-presigner^3.1102.0
Folders
server/storage/
Put this in your repo
Adds Cloudflare R2 to a project this tool generated, without starting over. Files you have hand-edited are left alone.