ShipVeryFastShipVeryFast
Documentation

Environment variables reference

Every environment variable ShipVeryFast reads, whether it is required or optional, the exact Zod rule that validates it, and where to find its value. The schema lives in libs/config.ts and the full set of placeholders lives in .env.example.

Validated at import time

libs/config.ts runs envSchema.parse(process.env) the moment the module is imported. If a required variable is missing or malformed (for example a SUPABASE_URL that is not a valid URL), the app throws on startup rather than failing later at runtime. Copy .env.example to .env.local and fill in every required value before running npm run dev.

The starting point: copy .env.example

The repository ships a complete .env.example. Copy it to .env.local and replace each placeholder with a real credential.

# Copy this file to .env.local and fill in your credentials.

# Supabase
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key

# NextAuth
NEXTAUTH_SECRET=your_nextauth_secret
NEXTAUTH_URL=http://localhost:3000

# CSRF Protection
CSRF_SECRET=your_csrf_secret_key_32_chars_minimum

# Stripe
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_PUBLIC_KEY=your_stripe_public_key
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret

# Stripe price IDs for subscription plans
STRIPE_PRICE_BASIC=your_stripe_price_basic_id
STRIPE_PRICE_PRO=your_stripe_price_pro_id
STRIPE_PRICE_ENTERPRISE=your_stripe_price_enterprise_id

# Mailgun
MAILGUN_API_KEY=your_mailgun_api_key
MAILGUN_DOMAIN=your_mailgun_domain
MAILGUN_FROM_EMAIL=your_default_from_email

# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

# Admin access, comma-separated allowlist of admin emails.
ADMIN_EMAILS=admin@example.com

# Magic Links rate limiting
RATE_LIMIT_MAGICLINK_MAX=5
RATE_LIMIT_MAGICLINK_DURATION=3600

# AI providers (optional)
ANTHROPIC_API_KEY=your_anthropic_api_key
OPENAI_API_KEY=your_openai_api_key

The Zod schema in libs/config.ts

This is the single source of truth for what is required. Anything typed as z.string().optional() can be omitted; everything else must be present and valid.

// Central configuration loader
import { z } from 'zod';

const envSchema = z.object({
  SUPABASE_URL: z.string().url(),
  SUPABASE_ANON_KEY: z.string(),
  SUPABASE_SERVICE_ROLE_KEY: z.string(),
  NEXTAUTH_SECRET: z.string(),
  NEXTAUTH_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string(),
  STRIPE_PUBLIC_KEY: z.string(),
  STRIPE_WEBHOOK_SECRET: z.string(),
  STRIPE_PRICE_BASIC: z.string(),
  STRIPE_PRICE_PRO: z.string(),
  STRIPE_PRICE_ENTERPRISE: z.string().optional(),
  MAILGUN_API_KEY: z.string(),
  MAILGUN_DOMAIN: z.string(),
  MAILGUN_FROM_EMAIL: z.string().email(),
  GOOGLE_CLIENT_ID: z.string(),
  GOOGLE_CLIENT_SECRET: z.string(),
  RATE_LIMIT_MAGICLINK_MAX: z.coerce.number(),
  RATE_LIMIT_MAGICLINK_DURATION: z.coerce.number(),
  ANTHROPIC_API_KEY: z.string().optional(),
  OPENAI_API_KEY: z.string().optional(),
});

export const env = envSchema.parse(process.env);
export type Env = typeof env;

Note that CSRF_SECRET and ADMIN_EMAILS appear in .env.example but are not part of this schema. They are read directly elsewhere in the codebase and are not validated at startup. See Not Zod-validated but used below.

Supabase

All three Supabase variables are required. Find them in your Supabase Dashboard under Project Settings → API.

VariableZod ruleWhere to find it
SUPABASE_URLz.string().url()Project Settings → API → Project URL
SUPABASE_ANON_KEYz.string()Project Settings → API → anon / public key
SUPABASE_SERVICE_ROLE_KEYz.string()Project Settings → API → service_role key (server-only secret, never expose to the browser)

NextAuth

Both NextAuth variables are required. NEXTAUTH_SECRET signs sessions and JWTs; NEXTAUTH_URL is the canonical base URL used for OAuth callbacks.

VariableZod ruleWhere to find it
NEXTAUTH_SECRETz.string()Generate one yourself (see below)
NEXTAUTH_URLz.string().url()http://localhost:3000 in dev; your deployed URL in production

Generating NEXTAUTH_SECRET

Generate a strong random secret on the command line and paste the output into .env.local.

openssl rand -base64 32

Stripe

The four core Stripe variables plus STRIPE_PRICE_BASIC and STRIPE_PRICE_PRO are required. STRIPE_PRICE_ENTERPRISEis the only optional one, the Enterprise tier is often a "contact sales" plan rather than a fixed price, and getStripePriceId('enterprise') returns undefined when it is unset. The price IDs map to plan IDs in libs/pricingPlans.ts.

VariableRequired?Where to find it
STRIPE_SECRET_KEYRequiredStripe Dashboard → Developers → API keys → Secret key
STRIPE_PUBLIC_KEYRequiredDevelopers → API keys → Publishable key
STRIPE_WEBHOOK_SECRETRequiredWebhook endpoint config, or stripe listen for local dev
STRIPE_PRICE_BASICRequiredCreate a product/price in the Stripe Dashboard, paste its price ID
STRIPE_PRICE_PRORequiredAs above, for the Pro plan
STRIPE_PRICE_ENTERPRISEOptionalLeave unset for a "contact sales" Enterprise tier

For the full payments flow, see Payments.

Mailgun

All three Mailgun variables are required for transactional email. MAILGUN_FROM_EMAIL is validated as a real email address (z.string().email()), so a malformed value throws at startup.

VariableZod ruleWhere to find it
MAILGUN_API_KEYz.string()Mailgun Dashboard → API keys
MAILGUN_DOMAINz.string()Mailgun Dashboard → Sending → Domains
MAILGUN_FROM_EMAILz.string().email()The default From address for outgoing mail (must be a valid email)

More detail in Email.

Google OAuth

Both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are required and come from the same OAuth credential. Create an OAuth 2.0 Client ID in the Google Cloud Console under APIs & Services → Credentials.

VariableZod ruleWhere to find it
GOOGLE_CLIENT_IDz.string()Google Cloud Console → Credentials → OAuth 2.0 Client ID
GOOGLE_CLIENT_SECRETz.string()Same OAuth credential, Client secret field

See Authentication for the full sign-in setup.

Rate limiting

Both magic-link rate-limit variables are required and are coerced from strings to numbers (z.coerce.number()), so the raw string values in your .env.local are converted automatically. The defaults in .env.example allow 5 requests per hour.

VariableZod ruleDefaultMeaning
RATE_LIMIT_MAGICLINK_MAXz.coerce.number()5Max magic-link requests allowed within the window
RATE_LIMIT_MAGICLINK_DURATIONz.coerce.number()3600Window length in seconds (3600 = 1 hour)

For the wider rate-limiting design, see Rate limiting.

AI providers (optional)

The two AI keys are the only application-feature variables that are fully optional (z.string().optional()). The boilerplate runs without either, the in-app AI assistant degrades gracefully and only the providers you configure appear in the picker.

VariableZod ruleWhere to find it
ANTHROPIC_API_KEYz.string().optional()console.anthropic.com (Claude path, @anthropic-ai/sdk)
OPENAI_API_KEYz.string().optional()platform.openai.com (OpenAI path, openai SDK)

For how the provider layer uses these, see AI assistant.

Not Zod-validated but used

Two variables ship in .env.example but are deliberately not in the envSchema in libs/config.ts. They are read directly by the modules that need them, so they are not checked at startup, set them carefully.

These fail silently, not loudly

Because they are outside the Zod schema, a missing or wrong value will not throw on startup. ADMIN_EMAILS in particular fails closed: if it is unset, nobody is treated as an admin.

CSRF_SECRET

The CSRF token signing key, read by libs/csrf.ts. The comment in .env.example recommends a value of at least 32 characters (your_csrf_secret_key_32_chars_minimum). Generate one the same way you generated NEXTAUTH_SECRET.

openssl rand -base64 32

ADMIN_EMAILS

A comma-separated allowlist of admin email addresses, read by libs/admin.ts. It gates the security audit and alerts API routes. It fails closed: when unset, no one is an admin.

ADMIN_EMAILS=alice@example.com,bob@example.com

See CSRF protection and Security monitoring for how these are consumed.

Next steps