tx-agent-kit
Storage

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.

VariableRequiredDefaultDescription
R2_ACCESS_KEY_IDYesnoneS3-compatible access key from R2 API token
R2_SECRET_ACCESS_KEYYesnoneS3-compatible secret key from R2 API token
R2_BUCKET_NAMENoa shared dev bucketTarget bucket name
R2_ENDPOINTNoderived from R2_ACCOUNT_IDS3 API endpoint, auto-composed as https://{ACCOUNT_ID}.r2.cloudflarestorage.com when unset
R2_ACCOUNT_IDNo(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_ID lives 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_ID and R2_SECRET_ACCESS_KEY live 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 .env

Matching 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

BucketWhen to use
An existing shared dev bucketDay-one local dev + CI integration tests while you're still spiking features
<your-app>-stagingA project-owned bucket for staging deploys
<your-app>-prodA 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>-prod

Verify 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 dev

The 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:

SettingValueReason
regionautoRequired by R2
forcePathStyletrueEnsures bucket name is in the URL path
endpointR2_ENDPOINT env varPoints to the R2 S3 API
PageDescription
Storage OverviewArchitecture and design decisions
Secrets Management1Password CLI patterns

On this page