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_keyThe 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.
| Variable | Zod rule | Where to find it |
|---|---|---|
SUPABASE_URL | z.string().url() | Project Settings → API → Project URL |
SUPABASE_ANON_KEY | z.string() | Project Settings → API → anon / public key |
SUPABASE_SERVICE_ROLE_KEY | z.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.
| Variable | Zod rule | Where to find it |
|---|---|---|
NEXTAUTH_SECRET | z.string() | Generate one yourself (see below) |
NEXTAUTH_URL | z.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 32Stripe
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.
| Variable | Required? | Where to find it |
|---|---|---|
STRIPE_SECRET_KEY | Required | Stripe Dashboard → Developers → API keys → Secret key |
STRIPE_PUBLIC_KEY | Required | Developers → API keys → Publishable key |
STRIPE_WEBHOOK_SECRET | Required | Webhook endpoint config, or stripe listen for local dev |
STRIPE_PRICE_BASIC | Required | Create a product/price in the Stripe Dashboard, paste its price ID |
STRIPE_PRICE_PRO | Required | As above, for the Pro plan |
STRIPE_PRICE_ENTERPRISE | Optional | Leave 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.
| Variable | Zod rule | Where to find it |
|---|---|---|
MAILGUN_API_KEY | z.string() | Mailgun Dashboard → API keys |
MAILGUN_DOMAIN | z.string() | Mailgun Dashboard → Sending → Domains |
MAILGUN_FROM_EMAIL | z.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.
| Variable | Zod rule | Where to find it |
|---|---|---|
GOOGLE_CLIENT_ID | z.string() | Google Cloud Console → Credentials → OAuth 2.0 Client ID |
GOOGLE_CLIENT_SECRET | z.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.
| Variable | Zod rule | Default | Meaning |
|---|---|---|---|
RATE_LIMIT_MAGICLINK_MAX | z.coerce.number() | 5 | Max magic-link requests allowed within the window |
RATE_LIMIT_MAGICLINK_DURATION | z.coerce.number() | 3600 | Window 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.
| Variable | Zod rule | Where to find it |
|---|---|---|
ANTHROPIC_API_KEY | z.string().optional() | console.anthropic.com (Claude path, @anthropic-ai/sdk) |
OPENAI_API_KEY | z.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 32ADMIN_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.comSee CSRF protection and Security monitoring for how these are consumed.
Next steps
- Configuration & services , wiring up Supabase, Stripe, Mailgun and Google
- Configuration files , where
libs/config.tsfits among the project config - Installation, the full clone-to-running walkthrough
