Skip to content

Environment variables

Locally, one .env at the repository root configures everything; copy .env.example to start. The API checks its variables at startup and names every one that is wrong.

  • Empty means not set. STRIPE_SECRET_KEY= is the same as leaving the line out.
  • PORT stands in for API_PORT when API_PORT is not set. When both are, API_PORT wins.
  • CORS_ORIGINS defaults to WEB_URL. Only set it if something else calls the API from a browser. It can be set in .env.railway too.
  • One provider at a time. BILLING_PROVIDER is stripe or polar, and the other’s key must be empty.
  • Stripe’s two values go together. With STRIPE_SECRET_KEY set, STRIPE_WEBHOOK_SECRET is required.
  • OAuth providers need both halves: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, or neither. Same for GitHub.
  • The example AUTH_SECRET is refused in production.
Variable What Default Production
NODE_ENV development, test or production development The API image sets it
API_PORT Where the API listens PORT, then 3000 Leave it to PORT
LOG_LEVEL fatal to trace, or silent debug dev, info production, silent tests Optional
CORS_ORIGINS Browser origins allowed to call the API, comma-separated WEB_URL Rarely

See Traces and metrics.

Variable What Default Production
OTEL_EXPORTER_OTLP_ENDPOINT Base URL of an OpenTelemetry backend. Empty: telemetry off None Optional
OTEL_EXPORTER_OTLP_HEADERS name=value pairs it needs, comma-separated: its key None With it
OTEL_SERVICE_NAME The name traces and metrics are filed under api, or worker Rarely
Variable What Default Production
WEB_URL Public address of the app; emails and OAuth use it http://localhost:5173 Yes, https://
AUTH_SECRET Signs sessions. 32+ characters None: API won’t start Yes, generated
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET Sign-in with Google No Google button If offered
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET Sign-in with GitHub No GitHub button If offered
ADMIN_EMAILS Who can open the admin panel, comma-separated Nobody Yes
MOBILE_SCHEME The mobile app’s URL scheme, as in its app.json saasstack If you changed the scheme
EXPO_ACCESS_TOKEN Push, with enhanced security on in Expo None: not needed Rarely
Variable What Default Production
STRIPE_SECRET_KEY sk_… or rk_…, test or live Billing off, all on Free If you charge
STRIPE_WEBHOOK_SECRET whsec_…, proves webhooks are Stripe’s Billing off With the key
BILLING_PROVIDER stripe or polar stripe If you use Polar
POLAR_ACCESS_TOKEN An organization access token, polar_oat_… Billing off, all on Free With Polar
POLAR_WEBHOOK_SECRET Proves webhooks are Polar’s Billing off With the token
POLAR_SERVER sandbox or production production production
Variable What Default Production
AI_GATEWAY_API_KEY Vercel AI Gateway key Assistant is off If offered
AI_MODEL provider/model deepseek/deepseek-v4.1-flash Optional

See Product analytics, and Mixpanel or PostHog to pick one. The web and mobile apps read it from the API, so it is set on the API only.

Variable What Default Production
ANALYTICS_PROVIDER mixpanel or posthog mixpanel If PostHog
MIXPANEL_TOKEN Mixpanel project token Analytics is off If you want it
MIXPANEL_REGION Where its data is kept: us, eu, in us If not in the US
POSTHOG_KEY PostHog project API key, phc_… Analytics is off If you want it
POSTHOG_HOST US cloud, EU cloud (https://eu.i.posthog.com), own https://us.i.posthog.com If not the US one

See Error reporting. The web and mobile apps read their DSNs from the API.

Variable What Default Production
SENTRY_DSN A Sentry project’s DSN. Empty: nothing is reported None Recommended
SENTRY_WEB_DSN The web app’s own project SENTRY_DSN Optional
SENTRY_MOBILE_DSN The mobile app’s own project SENTRY_DSN Optional
SENTRY_RELEASE The version errors are filed under RAILWAY_GIT_COMMIT_SHA Rarely
SENTRY_AUTH_TOKEN Building the web app only: sends its source maps None: no maps are made Optional
SENTRY_ORG Building the web app only: your Sentry organization None With it
SENTRY_WEB_PROJECT Building the web app only: the project they go to None With it
Variable What Default Production
JOBS_WORKER Whether the API runs jobs itself true false only with a separate worker
Variable What Default Production
DEMO_MODE true on a public demo: every page says it is one false true only on a demo
DEMO_RETURN_URL On a demo, where the banner’s “Back to …” link goes none Only on a demo
DEMO_ALL_FEATURES On a demo, true gives everyone every feature, whatever their plan false Only on a demo
PUBLIC_DEMO_RETURN_URL The same, for the bar on the marketing site none Site image build argument
Variable What Default Production
STAND_INS Stand-ins for Stripe and the model, for the journeys. Only on localhost false Never: deploy:check fails on it
Variable What Default Production
DATABASE_URL postgres://… None: API won’t start Yes, from host
PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE The database in parts, without DATABASE_URL None From host, on AWS
DATABASE_ORM drizzle, prisma or typeorm drizzle Same as dev
REDIS_URL redis://… redis://localhost:6379 Yes, from host
REDIS_PORT Local Docker Redis 6379 Not used
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_PORT Local Docker database saas, saas, saas, 5432 Not used

Change a POSTGRES_* value and change DATABASE_URL to match. Choose DATABASE_ORM once, before the first migration: see Database and ORMs.

Variable What Default Production
SMTP_HOST Mail server localhost Yes
SMTP_PORT Its port; also Mailpit’s locally 1025 Yes, usually 587
SMTP_USER, SMTP_PASSWORD Login None Yes
MAIL_FROM Sender SaaS Stack <hello@example.com> Yes, on a verified domain
CONTACT_EMAIL Where the contact form sends messages none: the form is off If you want the form
MAILPIT_UI_PORT Where you read caught email 8025 Not used
Variable What Default Production
WEB_PORT Web dev server port 5173 Not used
VITE_API_URL Where the dev server sends /api http://localhost:3000 Not used; Caddy uses API_URL
SITE_PORT Site dev server port 4321 Not used
SITE_URL The site’s address, for canonical links and sitemap http://localhost:4321 Site image build argument
PUBLIC_APP_URL Where the site’s “Sign in” goes http://localhost:5173 Site image build argument
EXPO_PUBLIC_API_URL Where the mobile app finds the API http://localhost:3000 Your web app’s address
VITE_API_BASE_URL The API’s address, if it is not under /api of the web app none: the same address Only without the web image’s proxy

On a phone, localhost is the phone: use your machine’s network address.

Variable Read by What Default
PORT api, web, site Where to listen 3000 api, 8080 web and site
API_URL web, site The API’s private address, for /api and the contact form http://localhost:3000

Besides the API’s variables above, Railway reads:

Variable What Default
RAILWAY_PROJECT Project it may change Required
RAILWAY_REPO owner/name on GitHub This folder’s origin
RAILWAY_BRANCH Branch deployed main
WEB_DOMAIN, SITE_DOMAIN Your own domains, without https:// Railway’s addresses
RAILWAY_WORKER true for a separate worker service Jobs run in the API

Empty values there are left as they are on Railway.

  1. Add it to apps/api/src/config/env.ts and .env.example, with a comment.
  2. Read it with @Inject(ENV) env: Env, never process.env.
  3. If production needs it, add it to YOURS in .railway/railway.ts and to .env.railway.example.