Cloudflare R2
S3-compatible object storage with no egress fees, accessed through presigned URLs.
The rule
This is the whole text, exactly as your agent receives it. Nothing is held back for the paid tier.
Credentials
- Server-side only. R2 keys are account-scoped; there is no client-safe variant. Never in the app, never in an
{{envPrefix}}*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.
6 formats, one per tool
Each tab is the file that tool actually reads, at the path it actually looks in. Knowing where each one looks is most of the work of supporting it.
---
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 `{{envPrefix}}*` 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.
Hand-written by the module author, frontmatter and all. It is the source the four derived formats are rendered from, so a correction lands here first.
What else this module writes
The rule is one file of several. Selecting Cloudflare R2 contributes all of this too — merged with every other module you pick, with conflicts resolved rather than duplicated.
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.Advisory history
Every time this rule turned out to be wrong, and what we did about it.
This rule has been accurate since it was published. That is a fact about the rule, not a promise about the future — which is the whole reason this section exists.
Pro tells you the day a correction lands that affects a repo you actually have.
See what Pro adds →The wizard picks the rest of the stack with you, writes all 6 formats, and leaves a manifest so check can tell you when any of it drifts.