Typesafe Env Variables
Environment variables are validated at startup using a Zod schema. If a required variable is missing, the server fails
fast with a clear error message instead of crashing later with a cryptic undefined.
Env file locations
| File | Purpose |
|---|---|
apps/backend/.env | API secrets and service URLs (never commit) |
apps/backend/.env.example | Documented defaults for the team |
apps/frontend/.env | Public VITE_* values baked into the bundle |
apps/frontend/.env.example | Frontend defaults |
apps/admin/.env | Admin app VITE_* values (after a starter pack) |
docker-compose.yml | Local Postgres, Valkey, and object storage ports |
After installing packs, run onboard to walk through the variables those packs need.
The env schema
Defined in apps/backend/src/config/env/env.config.ts. Packs merge extra schemas into this file (for example
workos.env.ts, betterauth.env.ts, stripe.env.ts).
import { z } from "zod";
export const envSchema = z.object({
FRONTEND_URL: z.string(),
SERVER_URL: z.string().optional(),
DATABASE_URL: z.string(),
VALKEY_URL: z.string(),
// Pack-gated keys are added when you install those packs
});
export type Env = z.infer<typeof envSchema>;How validation works
The validateEnv() function runs at server startup, before routes accept traffic:
export const validateEnv = () => {
try {
envSchema.parse(process.env);
} catch (err) {
if (err instanceof z.ZodError) {
const errorMessage = z.prettifyError(err);
throw new Error(`Missing environment variables:\n ${errorMessage}`, { cause: err });
}
}
};If any variable is missing or has the wrong type, you get a list of exactly what is wrong before the server tries to use them.
Base template variables
These ship with the free starter (before packs):
| Variable | Purpose |
|---|---|
FRONTEND_URL | Public frontend origin (CORS, email links, redirects) |
SERVER_URL | Public API origin when set |
DATABASE_URL | Postgres connection string |
VALKEY_URL | Valkey / Redis connection string |
RUSTFS_ENDPOINT | S3-compatible storage endpoint |
RUSTFS_ACCESS_KEY | Storage access key |
RUSTFS_SECRET_KEY | Storage secret key |
BUCKET_NAME | Default object storage bucket |
See Storage for how the S3 client uses the RUSTFS_* values.
Frontend variables
Frontend (and admin) variables are prefixed with VITE_ and accessed via globalThis._importMeta_.env. They are baked into the
bundle at build time. Do not put secrets here.
| Variable | Purpose |
|---|---|
VITE_API_BASE_URL | Backend origin the HyperFetch client calls |
const apiUrl = globalThis._importMeta_.env.VITE_API_BASE_URL;Pack-gated variables
Only required after you install the matching pack. Full setup lives on each pack page.
WorkOS starter
| Variable | Purpose |
|---|---|
WORKOS_CLIENT_ID | WorkOS application client ID |
WORKOS_API_KEY | WorkOS API key |
WORKOS_COOKIE_PASSWORD | Session cookie encryption password |
WORKOS_GOOGLE_OAUTH_CALLBACK | Google OAuth callback URL when using Google |
Better Auth starter
| Variable | Purpose |
|---|---|
BETTER_AUTH_URL | Origin Better Auth mounts on (usually the API origin) |
BETTER_AUTH_SECRET | Auth secret (openssl rand -base64 32) |
GOOGLE_CLIENT_ID | Google OAuth client ID (optional social login) |
GOOGLE_CLIENT_SECRET | Google OAuth client secret |
See SaaS Starter (Better Auth).
Admin (both starters)
| Variable | Purpose |
|---|---|
SUPER_ADMIN_EMAIL | Comma-separated bootstrap super-admin emails |
See Admin app.
Billing (Stripe)
| Variable | Purpose |
|---|---|
STRIPE_SECRET_KEY | Stripe API secret |
STRIPE_WEBHOOK_SECRET | Webhook signing secret |
STRIPE_PRICE_PRO | Price ID for the Pro plan |
STRIPE_PRICE_PREMIER | Price ID for the Premier plan |
See Billing (Stripe).
Email (Resend)
| Variable | Purpose |
|---|---|
RESEND_API_KEY | Resend API key for transactional email |
RESEND_FROM_DOMAIN | Domain used to build the From address |
Used by auth (password reset, verification), invitations, optional billing receipts, and notifications. See Mailing.
Observability (optional)
| Variable | Purpose |
|---|---|
SENTRY_DSN | Backend Sentry project DSN |
VITE_SENTRY_DSN | Frontend Sentry project DSN |
See Observability.
Adding a new env variable
- Add it to the Zod schema in
apps/backend/src/config/env/env.config.ts(or a pack-specific*.env.tsmerge) - Add it to
.env.exampleso other developers know about it - Add it to your local
.env - Use
z.string().optional()if the variable is not required in all environments
Every purchase and sponsorship funds my 8+ years of work on open source given freely to the community. It keeps the lights on, funds new packs, and keeps the ecosystem alive. Even a small tier means a lot. Thank you!
