SsuperslateDocs

Configuration

Assign every product setting to the correct runtime boundary without exposing secrets.

Configuration

Configuration is owned by the layer that consumes it. The server validates private and deployment values once; the browser receives only intentionally public VITE_* values; deployment systems own their secret stores; provider dashboards own provider resources. Do not turn a local .env file into a second configuration authority.

The exact variables and defaults live in apps/server/.env.example and apps/web/.env.example. They are executable examples, while this page explains when a group is required and where it belongs.

Configuration map

ConcernCanonical ownerRequired whenKey examples
Database and runtimeServer environment validated in apps/server/src/config/env.tsAlwaysDATABASE_URL, pool limits, ENVIRONMENT, PORT
Identity and browser originServer environment and browser public environmentAlways outside local developmentBETTER_AUTH_SECRET, FRONTEND_URL, SERVER_URL, VITE_API_URL, TRUSTED_PROXY_PROFILE
Deployment identityDeployment environment and immutable release artifactA deployed API existsDEPLOYMENT_CLOUD, DEPLOYMENT_ENVIRONMENT, RELEASE_ID, IMAGE_DIGEST
Transactional emailServer environment and Resend accountProduction email is sentRESEND_API_KEY, EMAIL_FROM, EMAIL_TEMPLATES_DIR
BillingServer environment and exactly one provider dashboardPaid access is enabledBILLING_PROVIDER, selected provider credential, webhook secret, product IDs, display prices
Object storageServer environment and selected storage accountUploads are enabledSTORAGE_PROVIDER, bucket values, R2/S3/GCS credentials
OAuthServer environment and provider dashboardGoogle sign-in is enabledGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, VITE_GOOGLE_CLIENT_ID
Observability and alertsServer or browser environment and selected serviceSentry or Slack is enabledSENTRY_PROJECT_IDS, browser VITE_SENTRY, build-only Sentry upload values, SLACK_BOT_TOKEN
Signup attributionBrowser public environment and marketing APISignup lead capture is enabledVITE_SIGNUP_ATTRIBUTION_CAMPAIGN, VITE_SIGNUP_ATTRIBUTION_REFERRAL_SOURCE
Product identity and policiesSource-owned browser configuration and public environmentBefore real usersBRAND_NAME, VITE_TERMS_URL, VITE_PRIVACY_URL, VITE_MARKETING_URL
Local-only demo sign-inLocal web environment and a local seeded accountDevelopment convenience onlyVITE_DEV_LOGIN_EMAIL, VITE_DEV_LOGIN_PASSWORD

VITE_* values are compiled into browser assets. They must never contain server credentials, webhook secrets, database URLs, private origins, or provider API keys.

Environment by purpose

EnvironmentPurposeRequired behavior
Local developmentBuild product behavior without third-party accountsPostgres and Better Auth work. Email uses the local log; billing, storage, Slack, Sentry, and Google remain visibly disabled unless configured completely.
TestProve isolated behavior in CI or a disposable databaseTest-only values, no external delivery, no shared database, and no production credentials.
StagingRehearse the exact deployment and provider boundarySeparate origins, database, secret store, and provider test/sandbox resources from production.
ProductionAccept real users and durable dataExact HTTPS origins, unique secret, selected proxy profile, verified sender, backup/restore proof, and complete live configuration for every enabled provider.

Do not point a preview deployment at the production API. Use a stable staging browser/API pair under the same registrable site when testing cookies.

Configure in dependency order

  1. Copy the two example environment files to ignored local files. Set only Postgres, the local Better Auth secret, and local origins first.
  2. Run the zero-key path from Setup. A local account, verification, reset, and magic link must work before adding provider credentials.
  3. Choose the production topology and set exact origins, proxy profile, deployment identity, and secret ownership as described in Deployment.
  4. Enable one optional capability at a time. Supply every value in its group, start the server, and run its failure and success smoke before enabling the next group.
  5. Record the variable owner, rotation owner, source dashboard, and last verified date in the deployment's private operations record. Record names and owners, never values, in tickets or release notes.

The add an environment variable path is required when you change the application configuration contract. It covers validation, examples, generation, deployment, documentation, and verification together.

Optional capability groups

CapabilityAll-or-nothing contractWhere to continue
BillingSelect polar, stripe, or dodo; provide that provider's credential, webhook secret, and at least one monthly or yearly product mapping. Leave the selected provider's integration values all unset for zero-key local work.Billing
StorageSelect one provider and provide the complete bucket, public URL, region, and matching credentials required by that provider.Object storage
GoogleProvide server client ID/secret, browser client ID, exact provider callback, and allowed origins together.Authentication
SentryProvide the public browser DSN and server allowlist together; build upload credentials belong only in CI or the protected build environment.Security
SlackProvide the server token only when sanitized operational alerts have a defined owner. Its absence is a valid disabled state.Notifications and marketing endpoints
AttributionSet a campaign to capture first-touch source_page and bounded UTM parameters in session storage, then submit them after password, magic-link, or OAuth signup. The referral source defaults to signup; leaving the campaign unset disables the behavior.Notifications and marketing endpoints

Attribution is retried after the authenticated session appears and removed from session storage only after the marketing endpoint accepts it. It never stores the email in browser storage. The marketing endpoint merges an existing lead instead of replacing its first-touch fields.

Read-only mode

READ_ONLY_MODE=true makes the server answer every unsafe method with 403 and the code READ_ONLY while continuing to serve reads. Use it for a migration window, an incident, or a public preview deployment. It defaults to false, and at that default nothing in the request path changes.

The guard fails closed: a route is blocked unless its path sits under /auth, /webhook, /marketing, or /tunnel. Those four stay writable because a read-only deployment still has to let people sign in and out, accept provider webhooks it must not make the provider retry, capture marketing form submissions, and forward browser error reports. A new domain added later is blocked by default rather than silently writable.

The browser shows a dialog the first time a write is refused. Its wording comes from optional public values, each falling back to the generic readOnly translation when unset:

ValueEffect when set
VITE_READ_ONLY_TITLEReplaces the dialog title
VITE_READ_ONLY_BODYReplaces the dialog body; \n renders as a line break
VITE_READ_ONLY_CTA_LABELLabels the call-to-action button
VITE_READ_ONLY_CTA_URLRenders the call-to-action button; absent means no button

Blocked writes raise no toast and are not reported to Sentry, because in this mode they are expected rather than exceptional.

Changing BILLING_PROVIDER after customers exist is not ordinary configuration. It requires an explicit entitlement migration and reconciliation plan; one deployment must not combine provider entitlements.

Safe configuration checks

The server fails at startup for invalid required values and partial selected billing or storage configuration. Validate the intended environment without printing secrets:

vp run --filter @app/server typecheck
vp run --filter @app/server test:unit
vp run --filter @app/server build

Then exercise the selected deployment's /ready endpoint and the enabled provider's real test or live smoke. Use Troubleshooting for the first observable failure instead of loosening origin, cookie, validation, or signature policy.

Values that must stay out of source control

Never commit real .env files, backend.hcl, .tfvars, state, cloud credentials, database URLs, webhook secrets, Better Auth secrets, OAuth secrets, provider API keys, signed URLs, or customer data. A name or public origin can be documented; a credential value cannot.

On this page