Skip to content

Environment Variables

Local Env Files

Different tools read different files; staging and production application Workers never load these files.

FileCommittedConsumer and purpose
.envYesShared local-development placeholders
.env.localNoVite, Astro, and local API overrides
.env.dev.local / .env.test.localNoOptional Drizzle development/test overrides
.env.staging.localNoStaging db:* commands; must contain DATABASE_URL
.env.production.localNoProduction db:* commands; must contain DATABASE_URL

Terraform needs no env file: each root in infra/envs/ names its HCP organization and workspace inline. See Infrastructure.

For normal local development, .env.local takes precedence over .env. Create it by copying .env and filling in real values:

bash
cp .env .env.local

WARNING

Never put real secrets in .env – it is committed to git. Use the applicable gitignored .local file for anything sensitive. Database commands named for staging or production read only their matching file and fail if it is absent.

Cloudflare Worker Bindings

In production, environment variables are set as Worker secrets or bindings – not from .env files. Configure them in the Cloudflare dashboard or via Wrangler:

bash
bun wrangler secret put BETTER_AUTH_SECRET --config apps/api/wrangler.jsonc --env=""

Database connections use Hyperdrive bindings (HYPERDRIVE_CACHED, HYPERDRIVE_UNCACHED) instead of raw connection strings. See Deployment for production setup.

For local development, Wrangler reads Hyperdrive connection strings from the CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_* variables in .env / .env.local.

Variable Reference

Application

VariableRequiredDescription
APP_NAMEYesDisplay name in emails, passkey prompts, and the app header
APP_ORIGINYesFull origin URL (e.g., http://localhost:5173)
API_ORIGINDev onlyVite proxy target (e.g., http://localhost:8787)
ENVIRONMENTYesdevelopment, staging, or production

Database

VariableRequiredDescription
DATABASE_URLdb:* onlyConnection string for Drizzle Kit; the Workers reach Postgres through the Hyperdrive bindings instead
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_CACHEDDev onlyHyperdrive cached connection for local dev
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_UNCACHEDDev onlyHyperdrive uncached connection for local dev

Use an unpooled connection string for all three – the Neon host without -pooler. Prepared statements stay enabled, and a transaction-mode pooler breaks them.

Authentication

VariableRequiredDescription
BETTER_AUTH_SECRETYesSecret for signing sessions and tokens. Generate with bunx auth@latest secret
GOOGLE_CLIENT_IDNoGoogle OAuth client ID (console)
GOOGLE_CLIENT_SECRETNoGoogle OAuth client secret

Google sign-in is optional – email OTP and passkeys work without it. Set both variables or neither; supplying one is a configuration error rather than a silent half-enabled provider.

See Authentication for provider setup details.

Email

VariableRequiredDescription
RESEND_API_KEYYesResend API key for sending emails
RESEND_EMAIL_FROMYesSender address (e.g., [email protected])

Billing (Optional)

Stripe billing is optional – with none of these set the app works normally, the subscription query reports enabled: false, and Stripe mutation endpoints return 404. Setting only some of the first four throws when authentication initializes, naming what is missing; STRIPE_PRO_ANNUAL_PRICE_ID is independently optional.

VariableRequiredDescription
STRIPE_SECRET_KEYNoStripe API secret key
STRIPE_WEBHOOK_SECRETNoStripe webhook signing secret
STRIPE_STARTER_PRICE_IDNoStripe Price ID for the Starter plan
STRIPE_PRO_PRICE_IDNoStripe Price ID for the Pro plan (monthly)
STRIPE_PRO_ANNUAL_PRICE_IDNoStripe Price ID for the Pro plan (annual)

See Billing for Stripe configuration.

Cloudflare

VariableRequiredDescription
CLOUDFLARE_ACCOUNT_IDDeploy onlyCloudflare account ID
CLOUDFLARE_API_TOKENDeploy onlyAPI token for Wrangler deployments