Source: https://www.hype-stack.dev/docs/backend/env-variables

# 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`](/docs/cli/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`).

```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:

```ts
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](/docs/backend/storage)                    |
| `RESEND_API_KEY`, `RESEND_FROM_DOMAIN`                      | Transactional email. See [Mailing](/docs/backend/mailing)                      |
| `SENTRY_DSN`                                                | Optional error reporting. See [Observability](/docs/development/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](/docs/development/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](/docs/mobile).

```ts
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 |

See [SaaS Starter (WorkOS)](/docs/packs-templates/packs/starter-saas-workos).

### 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)](/docs/packs-templates/packs/starter-auth-betterauth) and
[SaaS Starter (Better Auth)](/docs/packs-templates/packs/starter-saas-betterauth).

### Admin (every starter)

| Variable            | Purpose                                      |
| ------------------- | -------------------------------------------- |
| `SUPER_ADMIN_EMAIL` | Comma-separated bootstrap super-admin emails |

See [Admin app](/docs/packs-templates/packs/admin).

### Billing

One provider per project. Each has a secret, a webhook signing secret, and one id per plan.

| Provider                                                               | Variables                                                                                                                                  |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| [Stripe](/docs/packs-templates/packs/pack-billing-stripe)              | `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_PRO`, `STRIPE_PRICE_PREMIER`                         |
| [Lemon Squeezy](/docs/packs-templates/packs/pack-billing-lemonsqueezy) | `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_WEBHOOK_SECRET`, `LEMONSQUEEZY_VARIANT_PRO`, `LEMONSQUEEZY_VARIANT_PREMIER` |
| [Polar](/docs/packs-templates/packs/pack-billing-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](/docs/packs-templates/packs/pack-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](/docs/packs-templates/packs/pack-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](/docs/backend/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](/docs/development/observability).

## Adding a new env variable

1. Add it to the Zod schema in `apps/backend/src/config/env/env.config.ts` (or a pack-specific `*.env.ts` merge)
2. Add it to `.env.example` so other developers know about it, and to `.env.test` if tests need it
3. Add it to your local `.env`
4. 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`](/docs/cli/onboard), and groups it under a
section. See the [manifest reference](/docs/packs-templates/pack-manifest).
