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.mdchand-written for this tool
.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.