Environment Variables Reference
Complete reference guide for all environment variables used in the TanStack Start Drizzle SaaS Kit.
Configure your MakerKit 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. MakerKit uses .env files for development and platform-specific settings (Railway, Docker, etc.) for production.
This page is part of the Configuration documentation.
Quick Start
The minimum required variables to run locally:
# apps/web/.env.localDATABASE_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 viaimport.meta.env.VITE_*and are inlined at build time into both the server and client bundles. Never put secrets behindVITE_. - 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_'](seeapps/web/vite.config.ts), so onlyVITE_-prefixed vars are exposed to client code.
Reading variables
// Public value (client or server) — inlined at build timeconst siteUrl = import.meta.env.VITE_SITE_URL;// Server-only secret — server code onlyconst 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:
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 |
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.
Database
To connect to your database, set the DATABASE_URL environment variable. This is passed directly to Drizzle ORM for connection pooling and query execution.
# PostgreSQL connection stringDATABASE_URL="postgresql://user:password@host:port/database"The connection string format depends on your database provider. Drizzle supports PostgreSQL, MySQL, SQLite, and other databases through their respective drivers.
Authentication
The authentication secret is used by Better Auth to sign sessions. It must be a random string of at least 32 characters.
# Secret for signing sessions (min 32 characters)# Generate with: openssl rand -base64 32BETTER_AUTH_SECRET="your-random-secret-here"# Base URL of your applicationVITE_SITE_URL="http://localhost:3000" # Development# VITE_SITE_URL="https://yourdomain.com" # ProductionBETTER_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
# Application name (displayed in UI)VITE_PRODUCT_NAME="Makerkit"# Site URL (for absolute links)VITE_SITE_URL="http://localhost:3000"# Default languageVITE_DEFAULT_LOCALE="en"# Theme settingsVITE_DEFAULT_THEME_MODE="light" # or "dark" or "system"VITE_PRODUCT_NAME: The name of your application, displayed in the UIVITE_SITE_URL: The base URL of your applicationVITE_DEFAULT_LOCALE: The default language of your applicationVITE_DEFAULT_THEME_MODE: The default theme mode of your application
Account Mode
Account mode is used to determine the default context for the application.
# Account mode (see Account Modes documentation)VITE_ACCOUNT_MODE="hybrid" # personal-only | organizations-only | hybridVITE_ACCOUNT_MODE: The account mode of your application. See Account Modes for more information.
Authentication Features
The variables below are used to specify settings related to authentication:
# Enable email/password authenticationVITE_AUTH_PASSWORD=true# Enable magic link authenticationVITE_AUTH_MAGIC_LINK=false# OAuth providers (comma-separated)VITE_AUTH_OAUTH_PROVIDERS="google" # google,github,apple# Password requirementsVITE_PASSWORD_REQUIRE_SPECIAL_CHARS=falseVITE_PASSWORD_REQUIRE_NUMBERS=falseVITE_PASSWORD_REQUIRE_UPPERCASE=false# Display terms and conditions checkboxVITE_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX=falseVITE_AUTH_PASSWORD: Whether to enable email/password authentication. Default istrue.VITE_AUTH_MAGIC_LINK: Whether to enable magic link authentication. Default isfalse.VITE_AUTH_OAUTH_PROVIDERS: The OAuth providers to enable as a comma-separated list (ex.google,github,apple). Default isgoogle.VITE_PASSWORD_REQUIRE_SPECIAL_CHARS: Whether to require special characters in passwords. Default isfalse.VITE_PASSWORD_REQUIRE_NUMBERS: Whether to require numbers in passwords. Default isfalse.VITE_PASSWORD_REQUIRE_UPPERCASE: Whether to require uppercase letters in passwords. Default isfalse.VITE_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX: Whether to display the terms and conditions checkbox during sign-up. Default isfalse.
Feature Flags
The variables below are used to specify settings related to feature flags:
# OrganizationsVITE_ALLOW_USER_TO_CREATE_ORGANIZATION=true# Custom roles/permissionsVITE_ENABLE_CUSTOM_ROLES=falseVITE_ALLOW_USER_TO_CREATE_ORGANIZATION: Whether to allow users to create organizations. Default istrue.VITE_ENABLE_CUSTOM_ROLES: Whether to enable custom roles. Default isfalse. 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 for details.
Captcha
The captcha plugin adds bot protection to authentication flows using Cloudflare Turnstile:
# Captcha (Cloudflare Turnstile)TURNSTILE_SECRET_KEY=""VITE_CAPTCHA_SITE_KEY=""VITE_CAPTCHA_WIDGET_SIZE=invisibleTURNSTILE_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 toinvisible. Set tonormalorcompactwhen 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:
VITE_INVITATION_EXPIRES_IN=604800VITE_INVITATION_EXPIRES_IN: The expiration time for invitations in seconds. Default is604800(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.
# Email provider (nodemailer or resend)MAILER_PROVIDER="nodemailer"# Sender addressEMAIL_SENDER="Your App <noreply@yourdomain.com>"MAILER_PROVIDER: The email provider to use. Default isnodemailer.EMAIL_SENDER: The sender address for emails. Default isYour App <noreply@yourdomain.com>.
For Nodemailer (SMTP)
If you use nodemailer (which is used by default), you need to set the SMTP configuration:
EMAIL_HOST="smtp.example.com"EMAIL_PORT=587EMAIL_TLS=trueEMAIL_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:
MAILER_PROVIDER="resend"RESEND_API_KEY="re_your_api_key"MAILER_PROVIDER: Useresendto use Resend as the email provider.RESEND_API_KEY: The API key for Resend whenMAILER_PROVIDERis set toresend.
Storage Configuration
The Storage configuration uses unstorage 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):
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:
# Billing providerVITE_BILLING_PROVIDER="stripe" # or polar# StripeSTRIPE_SECRET_KEY="sk_test_..."STRIPE_WEBHOOK_SECRET="whsec_..."VITE_BILLING_PROVIDER: The billing provider to use. Default isstripe. Supported values arestripeandpolar.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:
VITE_BILLING_PROVIDER="polar"POLAR_ACCESS_TOKEN=********Monitoring & Analytics
The variables below are used to specify settings related to monitoring and analytics:
# 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:
CMS_CLIENT=keystatic# Keystatic configurationVITE_KEYSTATIC_CONTENT_PATH=./contentVITE_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 iskeystatic. Supported values arekeystaticandwordpress.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 islocal. At this time,localis the only supported value.VITE_KEYSTATIC_STORAGE_REPO: The repository for the CMS. Default isundefined.KEYSTATIC_STORAGE_BRANCH_PREFIX: The branch prefix for the CMS (server-only, unprefixed). Default isundefined.KEYSTATIC_PATH_PREFIX: The path prefix for the CMS (server-only, unprefixed). Default isapps/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 theVITE_prefix. Default isundefined.
Logger
The variables below are used to specify settings related to the logger:
LOGGER=pinoLOGGER: The logger to use. Possible values arepinoorconsole. Default ispino. Theconsolevalue falls back to the built-inconsolemodule 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:
VITE_SITE_URL="https://example.com"Usage:
// Works in both client and server code; inlined at build timeconst 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:
DATABASE_URL="postgresql://..."BETTER_AUTH_SECRET="secret"STRIPE_SECRET_KEY="sk_test_..."Usage:
// Only works in server functions, server routes, and loadersconst dbUrl = process.env.DATABASE_URL;Build & Performance Settings
The .env file ships these build/runtime placeholders:
# Enable React Compiler (experimental)ENABLE_REACT_COMPILER=false# Enable strict Content Security PolicyENABLE_STRICT_CSP=falseENABLE_REACT_COMPILER: Reserved flag for enabling the experimental React Compiler. Default isfalse.ENABLE_STRICT_CSP: Reserved flag for enabling a strict Content Security Policy. Default isfalse.
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:
# Enable version update notificationsVITE_ENABLE_VERSION_UPDATER=false# How often to check for updates (in seconds)VITE_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS=3600VITE_ENABLE_VERSION_UPDATER: Whether to show a dialog when a new version is deployed. Default isfalse.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 is3600(1 hour).
Keystatic Path Prefix
When using Keystatic with GitHub or Cloud storage, you may need to specify a path prefix:
# Path prefix for Keystatic content in your repositoryKEYSTATIC_PATH_PREFIX=apps/webKEYSTATIC_PATH_PREFIX: The directory path within your repository where Keystatic content is stored. Required when using GitHub storage kind. Default isapps/web.
Security Best Practices
1. Never Commit Secrets
# In .gitignore (already configured).env.local.env.*.local2. Use Different Secrets Per Environment
# DevelopmentBETTER_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:
# Site ConfigurationVITE_SITE_URL=VITE_PRODUCT_NAME=# EmailMAILER_PROVIDER=nodemailerEMAIL_SENDER=EMAIL_HOST=EMAIL_PORT=EMAIL_USER=EMAIL_PASSWORD=EMAIL_TLS=# BillingVITE_BILLING_PROVIDER=stripeSTRIPE_WEBHOOK_SECRET=STRIPE_SECRET_KEY=# DatabaseBETTER_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
.envfiles, restart withpnpm 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. Usehttps://example.comnothttps://example.com/. - Same secrets across environments: Using identical
BETTER_AUTH_SECRETin dev and production is a security risk. Generate unique secrets per environment. - Missing OAuth provider credentials: If you add
googletoVITE_AUTH_OAUTH_PROVIDERS, you must also setGOOGLE_CLIENT_IDandGOOGLE_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.
Frequently Asked Questions
Where should I put production environment variables?
Can I use different .env files for staging vs production?
Why is my environment variable not updating?
How do I debug which variables are loaded?
What is the difference between .env and .env.local?
Next: Account Modes →