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 |
apps/extension/.env | Browser extension VITE_* values |
apps/mobile/.env | EXPO_PUBLIC_* values inlined by Metro |
apps/backend/docker-compose.yml | Local Postgres, Valkey, and RustFS ports |
create copies every .env.example to .env for you, and generates the secrets packs mark as generated.
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) |
ADMIN_URL | Public admin app origin (CORS) |
SERVER_URL | Public API origin when set |
DATABASE_URL | Postgres connection string |
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB | What Docker Compose boots Postgres with; DATABASE_URL is built from them |
VALKEY_URL, VALKEY_PASSWORD | Valkey / Redis connection |
RUSTFS_ENDPOINT, RUSTFS_ACCESS_KEY, RUSTFS_SECRET_KEY | S3-compatible storage. See Storage |
RESEND_API_KEY, RESEND_FROM_DOMAIN | Transactional email. See Mailing |
SENTRY_DSN | Optional error reporting. See Observability |
Every origin variable needs its scheme (https://app.example.com, not app.example.com). A bare host breaks CORS and
OAuth callbacks in ways that look unrelated; see Troubleshooting.
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 |
VITE_APP_TYPE | web or electron, so the shell knows which chrome to render |
VITE_ENVIRONMENT | development, staging, or production, for Sentry and logging |
VITE_SENTRY_DNS | Optional Sentry DSN for the web app |
VITE_SENTRY_AUTH_TOKEN | Optional, build-time only, for uploading source maps |
The mobile app reads the same ideas as EXPO_PUBLIC_API_BASE_URL, EXPO_PUBLIC_ENVIRONMENT, and
EXPO_PUBLIC_SENTRY_DNS; see Mobile.
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 starters (free and SaaS)
| Variable | Purpose |
|---|---|
BETTER_AUTH_URL | Origin Better Auth mounts on (usually the API origin) |
BETTER_AUTH_SECRET | Auth secret. The CLI generates one at install time |
GOOGLE_CLIENT_ID | Google OAuth client ID (optional social login) |
GOOGLE_CLIENT_SECRET | Google OAuth client secret |
MOBILE_OAUTH_REDIRECT_URL | Deep link the mobile app returns to after OAuth (default hypestack://callback) |
See Auth Starter (Better Auth) and SaaS Starter (Better Auth).
Admin (every starter)
| Variable | Purpose |
|---|---|
SUPER_ADMIN_EMAIL | Comma-separated bootstrap super-admin emails |
See Admin app.
Billing
One provider per project. Each has a secret, a webhook signing secret, and one id per plan.
| Provider | Variables |
|---|---|
| Stripe | STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_PRO, STRIPE_PRICE_PREMIER |
| Lemon Squeezy | LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_WEBHOOK_SECRET, LEMONSQUEEZY_VARIANT_PRO, LEMONSQUEEZY_VARIANT_PREMIER |
| Polar | POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_SERVER (sandbox while testing), POLAR_PRODUCT_PRO, POLAR_PRODUCT_PREMIER |
Newsletter
| Variable | Purpose |
|---|---|
RESEND_WEBHOOK_SECRET | Verifies Resend delivery, bounce, and complaint events on /newsletter/webhooks/resend |
NEWSLETTER_SEND_RATE | Emails per second the dispatcher sends (default 8) |
NEWSLETTER_SEND_CONCURRENCY | Parallel sends (default 8) |
See Newsletter.
AI chat
| Variable | Purpose |
|---|---|
OPENAI_API_KEY | Required. Text, transcription, speech, realtime voice, embeddings |
ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY | Optional, for the other text model providers |
AI_TEXT_MODEL, AI_TRANSCRIPTION_MODEL, AI_SPEECH_MODEL, AI_REALTIME_MODEL | Model ids as provider:model |
AI_STORAGE_BUCKET | Private bucket for uploads and generated images (default ai-private) |
AI_MAX_OUTPUT_TOKENS, AI_MAX_RUN_SECONDS, AI_MAX_VOICE_SECONDS, AI_USER_STORAGE_BYTES | Hard caps per run and per user |
KNOWLEDGE_EMBEDDING_MODEL, KNOWLEDGE_MAX_FILE_BYTES, KNOWLEDGE_MAX_PAGES, KNOWLEDGE_MAX_TEXT_CHARACTERS, KNOWLEDGE_INDEXING_CONCURRENCY | Document library limits and indexing concurrency |
See AI Chat.
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_DNS | Web app Sentry DSN (frontend, admin, extension) |
EXPO_PUBLIC_SENTRY_DNS | Mobile app Sentry 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, and to.env.testif tests need it - Add it to your local
.env - Use
z.string().optional()if the variable is not required in all environments
If you are writing a pack, declare the variable in the manifest's env field instead and the CLI patches all three
files, marks it required, generated, or optional for onboard, and groups it under a
section. See the manifest reference.