# Environment Variables Reference

> Complete reference guide for all environment variables used in the TanStack Start Prisma SaaS Kit.

*Canonical: https://makerkit.dev/docs/tanstack-prisma/configuration/environment-variables*

---

Configure your TanStack Start Prisma SaaS Kit application using environment variables for database connections, authentication secrets, billing providers, email, and more. This reference covers every variable with defaults and usage examples.

Environment variables are key-value pairs that configure application behavior without hardcoding values. The kit uses `.env` files for development and platform-specific settings (Railway, Docker, etc.) for production.

This page is part of the [Configuration documentation](./overview).

## Quick Start

The minimum required variables to run locally:

```bash
# apps/web/.env.local
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32"
VITE_SITE_URL="http://localhost:3000"
VITE_PRODUCT_NAME="My SaaS"
EMAIL_SENDER="My SaaS <noreply@example.com>"
```

**Use environment variables when:** the value changes per environment, contains secrets, or needs to be changed without redeploying.

**Use config files when:** you need TypeScript validation, complex objects, or the value is truly static.

**If unsure:** environment variables are the safe default for anything that might differ between dev and production.

## Public vs Server Variables (Vite)

This is a Vite-powered TanStack Start app. Environment variables follow Vite's conventions:

- **Public variables** use the `VITE_` prefix. They are read via `import.meta.env.VITE_*` and are **inlined at build time** into both the server and client bundles. Never put secrets behind `VITE_`.
- **Server-only secrets** stay **unprefixed** and are read from `process.env.*`. They are only available on the server (server functions, server routes, loaders).
- The Vite config sets `envPrefix: ['VITE_']` (see `apps/web/vite.config.ts`), so only `VITE_`-prefixed vars are exposed to client code.

### Reading variables

```typescript
// Public value (client or server) — inlined at build time
const siteUrl = import.meta.env.VITE_SITE_URL;

// Server-only secret — server code only
const dbUrl = process.env.DATABASE_URL;
```

For code that resolves a variable by a dynamic name, use the `env()` helper from `@kit/shared/env`, which checks `import.meta.env` first and falls back to `process.env`:

```typescript
import { env } from '@kit/shared/env';

const appHome = env('VITE_APP_HOME_PATH') ?? '/dashboard';
```

### When to Use Each Approach

| Context | Method | Visibility |
|---------|--------|------------|
| Public config (client + server) | `import.meta.env.VITE_*` | Inlined into the bundle at build time |
| Server-only secrets | `process.env.*` | Server only, never shipped to the browser |
| Dynamic-name lookup | `env('NAME')` from `@kit/shared/env` | Resolves `import.meta.env` then `process.env` |

{% callout title="Build-time inlining" %}
Because `VITE_*` values are inlined at build time, changing a public variable requires a rebuild. For Docker "build once, deploy many" of public values, you must rebuild per environment or move the value to a server-read (unprefixed) variable. Server secrets read from `process.env` are evaluated at runtime and can change per deployment without a rebuild.
{% /callout %}

---

### Database

To connect to your database, set the `DATABASE_URL` environment variable. This is passed directly to [Prisma](https://www.prisma.io/docs/orm/overview/databases/postgresql) for connection pooling and query execution.

```bash
# PostgreSQL connection string
DATABASE_URL="postgresql://user:password@host:port/database"
```

This kit is configured for PostgreSQL. While Prisma supports other databases, the schema and migrations assume a PostgreSQL provider.

### Authentication

The authentication secret is used by [Better Auth](https://www.better-auth.com/) to sign sessions. It must be a random string of at least 32 characters.

```bash
# Secret for signing sessions (min 32 characters)
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET="your-random-secret-here"

# Base URL of your application
VITE_SITE_URL="http://localhost:3000"  # Development
# VITE_SITE_URL="https://yourdomain.com"  # Production
```

- `BETTER_AUTH_SECRET`: The authentication secret used by Better Auth to sign sessions. Must be a random string of at least 32 characters.
- `VITE_SITE_URL`: The base URL of your application. When in production, it must use a valid HTTPS URL.

## Application Settings

```bash
# Application name (displayed in UI)
VITE_PRODUCT_NAME="Makerkit"

# Site URL (for absolute links)
VITE_SITE_URL="http://localhost:3000"

# Default language
VITE_DEFAULT_LOCALE="en"

# Theme settings
VITE_DEFAULT_THEME_MODE="light"  # or "dark" or "system"
```

- `VITE_PRODUCT_NAME`: The name of your application, displayed in the UI
- `VITE_SITE_URL`: The base URL of your application
- `VITE_DEFAULT_LOCALE`: The default language of your application
- `VITE_DEFAULT_THEME_MODE`: The default theme mode of your application

### Account Mode

Account mode is used to determine the default context for the application.

```bash
# Account mode (see Account Modes documentation)
VITE_ACCOUNT_MODE="hybrid"  # personal-only | organizations-only | hybrid
```

- `VITE_ACCOUNT_MODE`: The account mode of your application. See [Account Modes](../configuration/account-modes) for more information.


### Authentication Features

The variables below are used to specify settings related to authentication:

```bash
# Enable email/password authentication
VITE_AUTH_PASSWORD=true

# Enable magic link authentication
VITE_AUTH_MAGIC_LINK=false

# OAuth providers (comma-separated)
VITE_AUTH_OAUTH_PROVIDERS="google"  # google,github,apple

# Password requirements
VITE_PASSWORD_REQUIRE_SPECIAL_CHARS=false
VITE_PASSWORD_REQUIRE_NUMBERS=false
VITE_PASSWORD_REQUIRE_UPPERCASE=false

# Display terms and conditions checkbox
VITE_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX=false
```

- `VITE_AUTH_PASSWORD`: Whether to enable email/password authentication. Default is `true`.
- `VITE_AUTH_MAGIC_LINK`: Whether to enable magic link authentication. Default is `false`.
- `VITE_AUTH_OAUTH_PROVIDERS`: The OAuth providers to enable as a comma-separated list (ex. `google,github,apple`). Default is `google`.
- `VITE_PASSWORD_REQUIRE_SPECIAL_CHARS`: Whether to require special characters in passwords. Default is `false`.
- `VITE_PASSWORD_REQUIRE_NUMBERS`: Whether to require numbers in passwords. Default is `false`.
- `VITE_PASSWORD_REQUIRE_UPPERCASE`: Whether to require uppercase letters in passwords. Default is `false`.
- `VITE_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX`: Whether to display the terms and conditions checkbox during sign-up. Default is `false`.

### Feature Flags

The variables below are used to specify settings related to feature flags:

```bash
# Organizations
VITE_ALLOW_USER_TO_CREATE_ORGANIZATION=true

# Custom roles/permissions
VITE_ENABLE_CUSTOM_ROLES=false
```

- `VITE_ALLOW_USER_TO_CREATE_ORGANIZATION`: Whether to allow users to create organizations. Default is `true`.
- `VITE_ENABLE_CUSTOM_ROLES`: Whether to enable custom roles. Default is `false`. Please bear in mind this is currently an experimental feature and is not yet available for usage.

**Note:** Organization/team account settings are controlled by `VITE_ACCOUNT_MODE`. See [Account Modes](./account-modes) for details.

### Captcha

The captcha plugin adds bot protection to authentication flows using Cloudflare Turnstile:

```bash
# Captcha (Cloudflare Turnstile)
TURNSTILE_SECRET_KEY=""
VITE_CAPTCHA_SITE_KEY=""
VITE_CAPTCHA_WIDGET_SIZE=invisible
```

- `TURNSTILE_SECRET_KEY`: The secret key for Cloudflare Turnstile.
- `VITE_CAPTCHA_SITE_KEY`: The site key for Cloudflare Turnstile.
- `VITE_CAPTCHA_WIDGET_SIZE`: Controls the Turnstile widget's visual appearance. Defaults to `invisible`. Set to `normal` or `compact` when using a **Managed** site key so the checkbox is shown to users.

Both keys are not provided/enabled by default.

### Invitation Settings

The variables below are used to specify settings related to invitations:

```bash
VITE_INVITATION_EXPIRES_IN=604800
```

- `VITE_INVITATION_EXPIRES_IN`: The expiration time for invitations in seconds. Default is `604800` (7 days).

### Email Configuration

The variables below are used to specify settings related to email configuration. If you use `nodemailer` (which is used by default), refer to the `SMTP` settings. If you use `resend`, refer to the Resend settings.

```bash
# Email provider (nodemailer or resend)
MAILER_PROVIDER="nodemailer"

# Sender address
EMAIL_SENDER="Your App <noreply@yourdomain.com>"
```

- `MAILER_PROVIDER`: The email provider to use. Default is `nodemailer`.
- `EMAIL_SENDER`: The sender address for emails. Default is `Your App <noreply@yourdomain.com>`.

#### For Nodemailer (SMTP)

If you use `nodemailer` (which is used by default), you need to set the SMTP configuration:

```bash
EMAIL_HOST="smtp.example.com"
EMAIL_PORT=587
EMAIL_TLS=true
EMAIL_USER="your_username"
EMAIL_PASSWORD="your_password"
```

- `EMAIL_HOST`: The SMTP host for Nodemailer.
- `EMAIL_PORT`: The SMTP port for Nodemailer.
- `EMAIL_TLS`: Whether to use TLS for Nodemailer.
- `EMAIL_USER`: The username for Nodemailer.
- `EMAIL_PASSWORD`: The password for Nodemailer.

#### For Resend

If you use `resend`, you need to set the Resend API key:

```bash
MAILER_PROVIDER="resend"
RESEND_API_KEY="re_your_api_key"
```

- `MAILER_PROVIDER`: Use `resend` to use Resend as the email provider.
- `RESEND_API_KEY`: The API key for Resend when `MAILER_PROVIDER` is set to `resend`.

### Storage Configuration

The Storage configuration uses [unstorage](https://unstorage.dev) as a unified interface for storage providers. This means that you can use the same API to interact with different storage providers.

During local development, we store files in the `./public/storage.dev` folder:

**Local Storage (Development):**
```bash
STORAGE_PROVIDER="fs"
STORAGE_BASE_URL="/storage.dev"
```

If you use AWS S3 or any S3-compatible provider (such as Cloudflare R2) you can use the following configuration:

```
STORAGE_S3_ACCESS_KEY_ID="your-access-key"
STORAGE_S3_SECRET_ACCESS_KEY="your-secret-key"
STORAGE_S3_ENDPOINT="your-endpoint"
STORAGE_S3_BUCKET="your-bucket"
STORAGE_S3_REGION="your-region"
```

- `STORAGE_S3_ACCESS_KEY_ID`: The access key ID for the S3 bucket.
- `STORAGE_S3_SECRET_ACCESS_KEY`: The secret access key for the S3 bucket.
- `STORAGE_S3_ENDPOINT`: The endpoint for the S3 bucket.
- `STORAGE_S3_BUCKET`: The name of the S3 bucket.
- `STORAGE_S3_REGION`: The region of the S3 bucket.

### Billing

The variables below are used to specify settings related to billing:

```bash
# Billing provider
VITE_BILLING_PROVIDER="stripe" # or polar

# Stripe
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
```

- `VITE_BILLING_PROVIDER`: The billing provider to use. Default is `stripe`. Supported values are `stripe` and `polar`.
- `STRIPE_SECRET_KEY`: The secret key for Stripe.
- `STRIPE_WEBHOOK_SECRET`: The webhook secret for Stripe.

If using Polar, please provide the following environment variables:

```bash
VITE_BILLING_PROVIDER="polar"
POLAR_ACCESS_TOKEN=********
```

### Monitoring & Analytics

The variables below are used to specify settings related to monitoring and analytics:

```bash
# Monitoring provider (leave empty for console fallback)
VITE_MONITORING_PROVIDER=
```

- `VITE_MONITORING_PROVIDER`: The monitoring provider to use. No third-party provider ships by default; leave empty to use the console/null fallback. Register your own provider to enable it (see the custom monitoring provider guide).

## CMS

The variables below are used to specify settings related to the CMS:

```bash
CMS_CLIENT=keystatic

# Keystatic configuration
VITE_KEYSTATIC_CONTENT_PATH=./content
VITE_KEYSTATIC_STORAGE_KIND=local

# Keystatic storage options (for GitHub/Cloud storage)
# VITE_KEYSTATIC_STORAGE_REPO=
# KEYSTATIC_STORAGE_PROJECT=
# KEYSTATIC_STORAGE_BRANCH_PREFIX=
# KEYSTATIC_PATH_PREFIX=

# Keystatic GitHub token (SENSITIVE: Set in .env.local or deployment environment)
# KEYSTATIC_GITHUB_TOKEN=
```

- `CMS_CLIENT`: The CMS client to use. Default is `keystatic`. At this time, `keystatic` is the only supported value.
- `VITE_KEYSTATIC_CONTENT_PATH`: The path to the content for the CMS. Default is `./content`.
- `VITE_KEYSTATIC_STORAGE_KIND`: The storage kind for the CMS. Default is `local`. At this time, `local` is the only supported value.
- `VITE_KEYSTATIC_STORAGE_REPO`: The repository for the CMS. Default is `undefined`.
- `KEYSTATIC_STORAGE_BRANCH_PREFIX`: The branch prefix for the CMS (server-only, unprefixed). Default is `undefined`.
- `KEYSTATIC_PATH_PREFIX`: The path prefix for the CMS (server-only, unprefixed). Default is `apps/web`.
- `KEYSTATIC_GITHUB_TOKEN`: The GitHub token for the CMS when using GitHub as the storage provider. This is a sensitive value and should not use the `VITE_` prefix. Default is `undefined`.

## Logger

The variables below are used to specify settings related to the logger:

```bash
LOGGER=pino
```

- `LOGGER`: The logger to use. Possible values are `pino` or `console`. Default is `pino`. The `console` value falls back to the built-in `console` module in Node.js.

## Environment Files

### .env

**Purpose:** Public, non-sensitive configuration shared across all environments

**Committed to git:** Yes

**Contains:**
- Public configuration
- Feature flags
- Default settings
- Paths and URLs (without secrets)

### .env.development

**Purpose:** Development-specific configuration

**Committed to git:** Yes

**Contains:**
- Development database URLs
- Local email settings
- Development API keys (test mode)

### .env.local

**Purpose:** Local secrets and overrides

**Committed to git:** NO (git-ignored)

**Contains:**
- Database credentials
- API keys and secrets
- Authentication secrets
- Personal configuration overrides

**Security:** Never commit this file!

### Production Environment Variables

For production deployments, you should set environment variables directly in your hosting platform (Vercel, Railway, Docker, etc.) rather than using a `.env.production` file. This is the recommended approach for:
- Database credentials
- API keys and secrets
- Authentication secrets
- Production URLs

## Public vs Private Variables

### Public Variables (VITE_*)

**Accessible:** Client and server

**Security:** Exposed in browser: please be careful not to expose sensitive information to the client.

**Use for:**
- Site URLs
- Application names
- Public API keys (ex. Stripe publishable key)
- Feature flags (non-sensitive)

**Example:**
```bash
VITE_SITE_URL="https://example.com"
```

**Usage:**
```typescript
// Works in both client and server code; inlined at build time
const siteUrl = import.meta.env.VITE_SITE_URL;
```

### Private Variables

**Accessible:** Server only

**Security:** Never exposed to browser

**Use for:**
- Database credentials
- API secrets
- Authentication secrets
- Private API keys

**Example:**
```bash
DATABASE_URL="postgresql://..."
BETTER_AUTH_SECRET="secret"
STRIPE_SECRET_KEY="sk_test_..."
```

**Usage:**
```typescript
// Only works in server functions, server routes, and loaders
const dbUrl = process.env.DATABASE_URL;
```

## Build & Performance Settings

The `.env` file ships these build/runtime placeholders:

```bash
# Enable strict Content Security Policy
ENABLE_STRICT_CSP=false
```

- `ENABLE_STRICT_CSP`: Reserved flag for enabling a strict Content Security Policy. Default is `false`.

Baseline security headers (`X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`) are applied to every response by the global request middleware in `apps/web/src/start.ts`, independent of these flags.

### Version Updater

The version updater checks for new deployments and prompts users to reload:

```bash
# Enable version update notifications
VITE_ENABLE_VERSION_UPDATER=false

# How often to check for updates (in seconds)
VITE_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS=3600
```

- `VITE_ENABLE_VERSION_UPDATER`: Whether to show a dialog when a new version is deployed. Default is `false`.
- `VITE_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS`: How frequently to check for new versions, in seconds. Must be between 1 and 86400 (1 second to 24 hours). Default is `3600` (1 hour).

### Keystatic Path Prefix

When using Keystatic with GitHub or Cloud storage, you may need to specify a path prefix:

```bash
# Path prefix for Keystatic content in your repository
KEYSTATIC_PATH_PREFIX=apps/web
```

- `KEYSTATIC_PATH_PREFIX`: The directory path within your repository where Keystatic content is stored. Required when using GitHub storage kind. Default is `apps/web`.

## Security Best Practices

### 1. Never Commit Secrets

```bash
# In .gitignore (already configured)
.env.local
.env.*.local
```

### 2. Use Different Secrets Per Environment

```bash
# Development
BETTER_AUTH_SECRET="dev_secret_123abc..."

# Production (different secret!)
BETTER_AUTH_SECRET="prod_secret_xyz789..."
```

## Minimum Environment Variables

The minimum required environment variables to run the application in a local environment are:

```bash
# Site Configuration
VITE_SITE_URL=
VITE_PRODUCT_NAME=

# Email
MAILER_PROVIDER=nodemailer
EMAIL_SENDER=
EMAIL_HOST=
EMAIL_PORT=
EMAIL_USER=
EMAIL_PASSWORD=
EMAIL_TLS=

# Billing
VITE_BILLING_PROVIDER=stripe
STRIPE_WEBHOOK_SECRET=
STRIPE_SECRET_KEY=

# Database
BETTER_AUTH_SECRET=
DATABASE_URL=
```

This is the set of variables that you **must have** to run the application - and excludes all the default values for the variables which the kit provides by default.

If you add certain values, such as `google` to `VITE_AUTH_OAUTH_PROVIDERS`, you must also set the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables - and so on.

Set these by editing `apps/web/.env` (public defaults) and `apps/web/.env.local` (secrets, git-ignored), or via your hosting platform's environment settings in production.

## Common Pitfalls

- **Forgetting to restart the dev server**: Vite reads environment variables at startup. After changing `.env` files, restart with `pnpm dev`.
- **Using `VITE_` for secrets**: Variables with this prefix are inlined into client JavaScript and visible in the browser. Never prefix API keys, database URLs, or auth secrets.
- **Trailing slash in `VITE_SITE_URL`**: Causes broken OAuth redirects and double-slash URLs. Use `https://example.com` not `https://example.com/`.
- **Same secrets across environments**: Using identical `BETTER_AUTH_SECRET` in dev and production is a security risk. Generate unique secrets per environment.
- **Missing OAuth provider credentials**: If you add `google` to `VITE_AUTH_OAUTH_PROVIDERS`, you must also set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`.
- **Unquoted values with special characters**: Wrap values containing `#`, `$`, or spaces in quotes: `EMAIL_PASSWORD="my#complex$pass"`.
- **Committing `.env.local`**: This file should be git-ignored. If you accidentally commit secrets, rotate them immediately.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Where should I put production environment variables?", "answer": "Set them directly in your hosting platform (Railway, Docker, etc.) rather than in files. This keeps secrets out of your repository and allows per-environment configuration."},
     {"question": "Can I use different .env files for staging vs production?", "answer": "Vite supports mode-based files like .env.development and .env.production (loaded by the build mode), but for multiple staging environments, use your hosting platform environment variable UI. Each deployment can have its own values."},
     {"question": "Why is my environment variable not updating?", "answer": "Three common causes: (1) You did not restart the dev server. (2) You are accessing a non-VITE_ variable in client code. (3) A public VITE_ value is inlined at build time and cached in a build artifact - rebuild for production changes."},
     {"question": "How do I debug which variables are loaded?", "answer": "Add console.log(process.env.YOUR_VAR) in a server function or server route. For client-side, log import.meta.env.VITE_YOUR_VAR in a component. Never log secrets in production."},
     {"question": "What is the difference between .env and .env.local?", "answer": ".env is committed to git and contains non-sensitive defaults. .env.local is git-ignored and contains secrets and local overrides. .env.local takes precedence."}
   ]
/%}

---

**Next:** [Account Modes →](./account-modes)
