coldstart

Environment Variables

Complete reference of all environment variables with Zod validation schemas

Coldstart generates a packages/env/ package with Zod-validated environment variables using @t3-oss/env. Every variable is validated at startup — if a required variable is missing or malformed, the app fails immediately with a clear error message.

Architecture

packages/env/
├── src/
│   ├── server.ts    # API env vars (@t3-oss/env-nextjs or @t3-oss/env-core)
│   ├── web.ts       # Web env vars (@t3-oss/env-nextjs)
│   └── native.ts    # Mobile env vars (plain Zod schema)
├── package.json
└── tsconfig.json

Each platform imports its own env module:

Usage in apps
// In apps/api
import { env } from "@scope/env/server";

// In apps/web
import { env } from "@scope/env/web";

// In apps/mobile
import { env } from "@scope/env/native";

Variables by platform

Core variables (always generated)

VariableZod SchemaRequiredDescription
NODE_ENVz.enum(["development", "production", "test"]).default("development")NoRuntime environment
DATABASE_URLz.string().url()YesPostgreSQL connection string (Neon)
BETTER_AUTH_SECRETz.string().min(32)YesAuth signing secret (32+ chars)
BETTER_AUTH_URLz.string().url()YesAuth base URL (API URL)

OAuth variables (per provider)

Generated for each OAuth provider selected:

VariableZod SchemaRequiredCondition
GOOGLE_CLIENT_IDz.string().min(1)YesGoogle OAuth enabled
GOOGLE_CLIENT_SECRETz.string().min(1)YesGoogle OAuth enabled
GITHUB_CLIENT_IDz.string().min(1)YesGitHub OAuth enabled
GITHUB_CLIENT_SECRETz.string().min(1)YesGitHub OAuth enabled
APPLE_CLIENT_IDz.string().min(1)YesApple OAuth enabled
APPLE_CLIENT_SECRETz.string().min(1)YesApple OAuth enabled

Billing variables (Stripe)

VariableZod SchemaRequiredCondition
STRIPE_SECRET_KEYz.string().startsWith("sk_")YesStripe billing
STRIPE_WEBHOOK_SECRETz.string().startsWith("whsec_")YesStripe billing

Billing variables (Polar)

VariableZod SchemaRequiredCondition
POLAR_ACCESS_TOKENz.string().min(1)YesPolar billing
POLAR_WEBHOOK_SECRETz.string().min(1)YesPolar billing

Mobile billing (RevenueCat webhook)

VariableZod SchemaRequiredCondition
REVENUECAT_WEBHOOK_AUTH_KEYz.string().min(1)YesBilling + mobile platform

Full Zod schema

packages/env/src/server.ts (Stripe + Google OAuth example)
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
    DATABASE_URL: z.string().url(),
    BETTER_AUTH_SECRET: z.string().min(32),
    BETTER_AUTH_URL: z.string().url(),
    GOOGLE_CLIENT_ID: z.string().min(1),
    GOOGLE_CLIENT_SECRET: z.string().min(1),
    STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
    STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
  },
  client: {},
  runtimeEnv: {
    NODE_ENV: process.env.NODE_ENV,
    DATABASE_URL: process.env.DATABASE_URL,
    BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
    BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
    GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
    GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
  },
});

Core variables

VariableZod SchemaRequiredDescription
NODE_ENVz.enum(["development", "production", "test"]).default("development")NoRuntime environment
NEXT_PUBLIC_API_URLz.string().url().optional()NoAPI base URL

Billing variables (Stripe)

VariableZod SchemaRequiredCondition
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYz.string().startsWith("pk_").optional()NoStripe billing

Analytics variables

VariableZod SchemaRequiredCondition
NEXT_PUBLIC_POSTHOG_KEYNoAnalytics enabled
NEXT_PUBLIC_POSTHOG_HOSTNoAnalytics enabled

PostHog variables are used directly via process.env in the PostHog init code, not through the @t3-oss/env schema. PostHog gracefully handles missing keys by not initializing.

Full Zod schema

packages/env/src/web.ts (Stripe example)
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url().optional(),
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_").optional(),
  },
  runtimeEnv: {
    NODE_ENV: process.env.NODE_ENV,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
  },
});

Core variables

VariableZod SchemaRequiredDescription
EXPO_PUBLIC_API_URLz.string().url().optional()NoAPI base URL

Billing variables (RevenueCat)

VariableZod SchemaRequiredCondition
EXPO_PUBLIC_REVENUECAT_IOS_KEYz.string().optional()NoBilling enabled
EXPO_PUBLIC_REVENUECAT_ANDROID_KEYz.string().optional()NoBilling enabled

Analytics variables

VariableZod SchemaRequiredCondition
EXPO_PUBLIC_POSTHOG_KEYNoAnalytics enabled
EXPO_PUBLIC_POSTHOG_HOSTNoAnalytics enabled

Full Zod schema

Mobile uses a plain Zod schema (not @t3-oss/env) because Expo doesn't support the Next.js env pattern:

packages/env/src/native.ts (with billing)
import { z } from "zod";

const schema = z.object({
  EXPO_PUBLIC_API_URL: z.string().url().optional(),
  EXPO_PUBLIC_REVENUECAT_IOS_KEY: z.string().optional(),
  EXPO_PUBLIC_REVENUECAT_ANDROID_KEY: z.string().optional(),
});

export const env = schema.parse({
  EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
  EXPO_PUBLIC_REVENUECAT_IOS_KEY: process.env.EXPO_PUBLIC_REVENUECAT_IOS_KEY,
  EXPO_PUBLIC_REVENUECAT_ANDROID_KEY: process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_KEY,
});

.env file locations

Each app has its own .env file. Coldstart generates .env.example files with placeholder values:

FileUsed byGit-tracked
apps/api/.envAPI serverNo (in .gitignore)
apps/api/.env.exampleReferenceYes
apps/web/.envNext.js web appNo (in .gitignore)
apps/web/.env.exampleReferenceYes
apps/mobile/.envExpo mobile appNo (in .gitignore)
apps/mobile/.env.exampleReferenceYes

How startup validation works

The env validation runs at import time. When your app starts and imports @scope/env/server, the Zod schema validates every variable immediately:

❌ Invalid environment variables:
  DATABASE_URL: Required
  BETTER_AUTH_SECRET: String must contain at least 32 character(s)
  STRIPE_SECRET_KEY: Invalid input: must start with "sk_"

This fail-fast approach catches configuration errors before any request is served.

If your API fails to start with env validation errors, check that all required variables are set in apps/api/.env. Copy from .env.example as a starting point.

Which variables depend on features

Not all variables are always generated. The env schema adapts to your project configuration:

Quick reference: all variables

VariablePlatformConditionRequired
NODE_ENVAPI, WebAlwaysNo (defaults to development)
DATABASE_URLAPIAlwaysYes
BETTER_AUTH_SECRETAPIAlwaysYes
BETTER_AUTH_URLAPIAlwaysYes
GOOGLE_CLIENT_IDAPIGoogle OAuthYes
GOOGLE_CLIENT_SECRETAPIGoogle OAuthYes
GITHUB_CLIENT_IDAPIGitHub OAuthYes
GITHUB_CLIENT_SECRETAPIGitHub OAuthYes
APPLE_CLIENT_IDAPIApple OAuthYes
APPLE_CLIENT_SECRETAPIApple OAuthYes
STRIPE_SECRET_KEYAPIStripe billingYes
STRIPE_WEBHOOK_SECRETAPIStripe billingYes
POLAR_ACCESS_TOKENAPIPolar billingYes
POLAR_WEBHOOK_SECRETAPIPolar billingYes
REVENUECAT_WEBHOOK_AUTH_KEYAPIBilling + mobileYes
NEXT_PUBLIC_API_URLWebAlwaysNo
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYWebStripe billingNo
NEXT_PUBLIC_POSTHOG_KEYWebAnalyticsNo
NEXT_PUBLIC_POSTHOG_HOSTWebAnalyticsNo
EXPO_PUBLIC_API_URLMobileAlwaysNo
EXPO_PUBLIC_REVENUECAT_IOS_KEYMobileBillingNo
EXPO_PUBLIC_REVENUECAT_ANDROID_KEYMobileBillingNo
EXPO_PUBLIC_POSTHOG_KEYMobileAnalyticsNo
EXPO_PUBLIC_POSTHOG_HOSTMobileAnalyticsNo

On this page