Storage Configuration
Environment variables, 1Password setup, and R2 bucket configuration for Cloudflare storage
Environment variables
All storage configuration lives in packages/infra/storage/src/env.ts. The getStorageEnv() function reads these values from process.env.
| Variable | Required | Default | Description |
|---|---|---|---|
R2_ACCESS_KEY_ID | Yes | none | S3-compatible access key from R2 API token |
R2_SECRET_ACCESS_KEY | Yes | none | S3-compatible secret key from R2 API token |
R2_BUCKET_NAME | No | a shared dev bucket | Target bucket name |
R2_ENDPOINT | No | derived from R2_ACCOUNT_ID | S3 API endpoint, auto-composed as https://{ACCOUNT_ID}.r2.cloudflarestorage.com when unset |
R2_ACCOUNT_ID | No | (local dev default) | Cloudflare account ID |
R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY are always required. R2_ENDPOINT is derived from R2_ACCOUNT_ID at runtime when not explicitly set.
1Password integration
Secrets use a two-vault split (see Secrets Management for the full rule):
api-keys(account-level) —R2_ACCOUNT_IDlives here because it identifies the Cloudflare account itself and is shared across every project that uses R2 on the same account.<project-services>/<env>(per-project, bucket-scoped) —R2_ACCESS_KEY_IDandR2_SECRET_ACCESS_KEYlive here because R2 IAM tokens are bucket-scoped and should stay with the project that owns the bucket.
New apps bootstrapped from this boilerplate commonly borrow an existing bucket's credentials until they provision their own. In that case the <project-services> vault referenced in the .env.example is the bucket owner's vault, not the new app's — that's intentional. When the new app creates its own bucket, it rotates to its own vault.
# Read the centralized account ID
op read "op://api-keys/Cloudflare/account_id"
# Read the bucket-owning project's credentials
op read "op://<project-services>/dev/R2_ACCESS_KEY_ID"
op read "op://<project-services>/dev/R2_SECRET_ACCESS_KEY"
# Generate a local .env from the committed template
op inject -i .env.example -o .envMatching references in .env.example
R2_ACCOUNT_ID=op://api-keys/Cloudflare/account_id
R2_ACCESS_KEY_ID=op://<project-services>/dev/R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY=op://<project-services>/dev/R2_SECRET_ACCESS_KEY
R2_BUCKET_NAME=<bucket-name>
R2_ENDPOINT=Check the actual .env.example at the root of the repo for the concrete vault and bucket in use today.
Bucket strategy
| Bucket | When to use |
|---|---|
| An existing shared dev bucket | Day-one local dev + CI integration tests while you're still spiking features |
<your-app>-staging | A project-owned bucket for staging deploys |
<your-app>-prod | A project-owned bucket for production |
Before going to production, provision project-specific buckets via Wrangler and rotate into <your-app>-services/<env> credentials:
wrangler r2 bucket create <your-app>-staging
wrangler r2 bucket create <your-app>-prodVerify with wrangler r2 bucket list.
Local development
Set the R2 credentials in your .env file or inject them via 1Password before running the API or worker:
# Preferred: generate .env from the op:// template
op inject -i .env.example -o .env
pnpm dev
# Or: inject on demand for a single command
op run --env-file=.env -- pnpm devThe storage package throws a clear error if credentials are missing, guiding you to set them up.
S3 client configuration
The underlying S3Client is configured for R2 compatibility:
| Setting | Value | Reason |
|---|---|---|
region | auto | Required by R2 |
forcePathStyle | true | Ensures bucket name is in the URL path |
endpoint | R2_ENDPOINT env var | Points to the R2 S3 API |
Related pages
| Page | Description |
|---|---|
| Storage Overview | Architecture and design decisions |
| Secrets Management | 1Password CLI patterns |