Skip to main content
Sabo uses environment variables to configure integrations with Supabase, Stripe, PostHog, and other services. This guide provides a complete reference of all variables, their purpose, and how to set them up.

Overview

Environment variables are key-value pairs that configure your application without hardcoding sensitive information. Sabo uses three types of environment files:
  • .env.local - Local development (not committed to git)
  • .env.test - Testing environment (Playwright E2E tests)
  • Production environment - Set in your hosting platform (Vercel, Netlify, etc.)
Never commit .env.local or .env.test to version control. These files contain sensitive credentials and should be added to .gitignore.

Variable Naming Convention

Sabo follows Next.js environment variable conventions:
  • NEXT_PUBLIC_* - Exposed to the browser (client-side code can access these)
  • Without NEXT_PUBLIC_ - Server-side only (API routes, server components)
Client-side code cannot access server-only variables. If you try to access process.env.STRIPE_SECRET_KEY in a client component, it will be undefined.

Required Variables

These variables are essential for Sabo to function. Your app will not work without them.

Core Application

NEXT_PUBLIC_SITE_URL
string
required
The base URL of your application. Used for generating absolute URLs, OAuth redirects, and email links.Local development:
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Production:
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
Used in:
  • OAuth callback URLs (src/app/(auth)/actions.ts)
  • Email verification links
  • Stripe Customer Portal return URL (src/app/api/customer_portal/route.ts)
Must match the URL configured in Supabase Authentication → URL Configuration. Mismatches will cause OAuth and email verification to fail.

Supabase (Authentication & Database)

NEXT_PUBLIC_SUPABASE_URL
string
required
Your Supabase project URL. Find this in your Supabase Dashboard → Project Settings → API.Format:
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
Used in:
  • Browser client (src/lib/supabase/client.ts)
  • Server client (src/lib/supabase/server.ts)
  • Middleware (src/lib/supabase/middleware.ts)
  • Test helpers (tests/e2e/helpers/auth.ts)
Each Supabase project has a unique subdomain. Copy the entire URL including https://.
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
string
required
Your Supabase project’s anonymous key (also called “anon key”). This is safe to expose to the browser.Format:
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Used in:
  • Browser client (src/lib/supabase/client.ts)
  • Server client (src/lib/supabase/server.ts)
  • Middleware (src/lib/supabase/middleware.ts)
  • Test helpers (tests/e2e/helpers/auth.ts)
Where to find: Supabase Dashboard → Project Settings → API → Project API keys → anon public
This key is safe to expose in client-side code. It respects Row Level Security (RLS) policies.
SUPABASE_SECRET_KEY
string
required
Your Supabase project’s secret key. This bypasses Row Level Security and should never be exposed to the browser.Format:
SUPABASE_SECRET_KEY=sbp_1234567890abcdef...
Used in:
  • Service client for bypassing RLS (src/lib/supabase/server.ts - createServiceClient())
  • Stripe webhook handler (src/app/api/webhooks/stripe/route.ts)
  • Polar webhook handler (src/app/api/webhooks/polar/route.ts)
Where to find: Supabase Dashboard → Project Settings → API → Project API keys → service_role secret
Server-only variable. Never prefix with NEXT_PUBLIC_. This key bypasses Row Level Security and can access all data. Only use for trusted server-side operations like webhooks.
Need deep dives on Supabase tables, clients, and auth flows? Review Database with Supabase and Auth with Supabase for end-to-end examples.

Stripe (Payments)

STRIPE_SECRET_KEY
string
required
Your Stripe secret API key. Used to create checkout sessions, manage subscriptions, and access the Stripe API.Test mode:
STRIPE_SECRET_KEY=sk_test_51A...
Production mode:
STRIPE_SECRET_KEY=sk_live_51A...
Used in:
  • Stripe client initialization (src/lib/payments/stripe.ts)
  • Creating checkout sessions (src/app/api/checkout_sessions/route.ts)
  • Creating Customer Portal sessions (src/app/api/customer_portal/route.ts)
  • Webhook verification (src/app/api/webhooks/stripe/route.ts)
Where to find: Stripe Dashboard → Developers → API keys → Secret key
Server-only variable. Keep this secret! It provides full access to your Stripe account.
STRIPE_WEBHOOK_SECRET
string
required
The webhook signing secret for verifying Stripe webhook events. Ensures webhook requests actually come from Stripe.Local development (Stripe CLI):
STRIPE_WEBHOOK_SECRET=whsec_...
Production:
STRIPE_WEBHOOK_SECRET=whsec_...
Used in:
  • Webhook signature verification (src/app/api/webhooks/stripe/route.ts)
How to get:
  • Local Development
  • Production
  1. Install Stripe CLI: brew install stripe/stripe-cli/stripe
  2. Login: stripe login
  3. Forward webhooks: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  4. Copy the whsec_... secret from the terminal output
Stripe CLI automatically forwards webhook events to your local server and provides a test signing secret.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
string
required
Your Stripe publishable API key. Safe to expose to the browser. Used for Stripe Checkout and payment forms.Test mode:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51A...
Production mode:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_51A...
Used in:
  • Client-side Stripe initialization
  • Stripe Checkout redirects
Where to find: Stripe Dashboard → Developers → API keys → Publishable key
Use test keys for development and live keys for production. Test keys start with sk_test_ and pk_test_.
Once your Stripe variables are in place, follow the dedicated Payments with Stripe guide to configure products, plans, and webhooks.

Polar (Payments)

POLAR_ACCESS_TOKEN
string
required
Server-side token used by Polar Checkout, Customer Portal, and webhook helpers.Format:
POLAR_ACCESS_TOKEN=polar_at_1234567890abcdef
Used in:
  • Checkout redirect handler (src/app/api/checkout/route.ts)
  • Customer portal handler (src/app/api/portal/route.ts)
  • Webhook handler + invoice helper (src/app/api/webhooks/polar/route.ts)
Where to find: Polar Dashboard → Settings → Developers → Access Tokens.
Treat this exactly like a Stripe secret key. Keep it server-only and rotate it if leaked.
POLAR_WEBHOOK_SECRET
string
required
Signing secret used to verify Polar webhook payloads.Format:
POLAR_WEBHOOK_SECRET=polar_wh_1234567890abcdef
How to get:
  1. Polar Dashboard → Settings → Webhooks → Add endpoint.
  2. Point to /api/webhooks/polar (ngrok URL in dev, production domain in prod).
  3. Copy the generated secret.
Used in: src/app/api/webhooks/polar/route.ts
Whenever you recreate the webhook endpoint (new ngrok URL or production domain), update this secret and restart the server.
Follow the Payments with Polar guide to finish product IDs, checkout metadata, and webhook setup.

Optional Variables

These variables enable additional features but are not required for basic functionality.

Stripe (Advanced)

NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY
string
The Stripe Price ID for your Pro plan monthly subscription.Format:
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY=price_1AbC2dEf3GhI4jK
Used in:
  • Plan configuration (src/lib/payments/plans.ts)
  • Pricing page for displaying correct checkout button
How to get:
  1. Create a product in Stripe Dashboard → Products
  2. Add a monthly recurring price
  3. Copy the Price ID (starts with price_)
If not set, the value from src/lib/payments/plans.ts is used as fallback. You can hardcode Price IDs in that file instead of using environment variables.
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY
string
The Stripe Price ID for your Pro plan yearly subscription.Format:
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY=price_2XyZ3wVu4TsR5qP
Used in:
  • Plan configuration (src/lib/payments/plans.ts)
  • Pricing page billing cycle toggle
Setup: Same as monthly Price ID, but set interval to “Yearly” when creating the price.
STRIPE_CUSTOMER_PORTAL_CONFIG_ID
string
The configuration ID for your Stripe Customer Portal. Allows customization of which features customers can manage.Format:
STRIPE_CUSTOMER_PORTAL_CONFIG_ID=bpc_1A2B3C4D...
Used in:
  • Customer Portal session creation (src/app/api/customer_portal/route.ts)
How to get:
  1. Go to Stripe Dashboard → Settings → Billing → Customer portal
  2. Create a new configuration or use the default
  3. Copy the configuration ID
What it controls:
  • Which payment methods customers can add/remove
  • Whether customers can cancel subscriptions
  • Custom branding and messaging
If not set, Stripe uses your account’s default Customer Portal configuration.

Polar (Advanced)

NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLY
string
Polar product ID for the monthly Pro tier. Rendered in the pricing UI and appended to /api/checkout.Format:
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLY=prod_monthly_123
Used in: Plan configuration + pricing checkout (src/lib/payments/plans.ts, src/components/marketing/pricing.tsx)
You can hardcode IDs directly in plans.ts, but environment variables make it easier to swap products per environment.
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY
string
Polar product ID for the yearly Pro tier.Format:
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY=prod_yearly_123
Used in: src/lib/payments/plans.ts, pricing toggle, webhook plan lookup helper.
POLAR_SANDBOX
boolean
Controls whether helper utilities call the sandbox or production Polar API (https://sandbox-api.polar.sh vs https://api.polar.sh).
# Development
POLAR_SANDBOX=true

# Production
POLAR_SANDBOX=false
Used in:
  • Webhook invoice helper (src/app/api/webhooks/polar/route.ts) — determines API base URL
  • Checkout handler — when modified to use environment-driven server selection
  • Customer portal (src/app/api/portal/route.ts) — uses NODE_ENV by default, but can be updated to respect POLAR_SANDBOX
The default boilerplate ships with server: "sandbox" hardcoded in /api/checkout for safety. To make it environment-driven, update the handler to use process.env.POLAR_SANDBOX === "true" ? "sandbox" : "production". See the Payments with Polar guide for details.
When unset, the webhook helper defaults to production (https://api.polar.sh). Explicitly set POLAR_SANDBOX=true for local development to avoid hitting live endpoints.

PostHog (Analytics)

NEXT_PUBLIC_POSTHOG_KEY
string
Your PostHog project API key. Required to enable PostHog analytics.Format:
NEXT_PUBLIC_POSTHOG_KEY=phc_1234567890abcdefghijklmnopqrstuvwxyz
Used in:
  • Client-side PostHog initialization (src/components/posthog-provider.tsx)
  • Server-side PostHog client (src/lib/posthog/server.ts)
Where to find: PostHog Dashboard → Project Settings → Project API Key
PostHog is completely optional. If this variable is not set, Sabo will function normally without analytics. Both NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST must be set for PostHog to activate.
NEXT_PUBLIC_POSTHOG_HOST
string
The PostHog host URL. Depends on your PostHog hosting choice.PostHog Cloud (US):
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
PostHog Cloud (EU):
NEXT_PUBLIC_POSTHOG_HOST=https://eu.posthog.com
Self-hosted:
NEXT_PUBLIC_POSTHOG_HOST=https://your-posthog-instance.com
Used in:
  • Client-side PostHog initialization (src/components/posthog-provider.tsx)
  • Server-side PostHog client (src/lib/posthog/server.ts)
Choose the region closest to your users for better performance. EU region is GDPR-compliant by default.
For implementation patterns (client/provider setup, feature flags, server events), see the Analytics with PostHog guide.

Testing (Playwright)

TEST_USER_EMAIL
string
Email address of a test user for E2E authentication tests.Format:
TEST_USER_EMAIL=[email protected]
Used in:
  • Authentication helper (tests/e2e/helpers/auth.ts)
  • Sign-in tests (tests/e2e/auth/sign-in.spec.ts)
  • Password reset tests (tests/e2e/auth/password-reset.spec.ts)
Setup:
  1. Create a test user in your Supabase database (manually or via Supabase Dashboard)
  2. Use a dedicated test email (not a real user account)
  3. Add email to .env.test file
Only set in .env.test, not .env.local. Keep test credentials separate from development credentials.
TEST_USER_PASSWORD
string
Password for the test user account.Format:
TEST_USER_PASSWORD=your_test_password_here
Used in:
  • Authentication helper (tests/e2e/helpers/auth.ts)
  • Sign-in tests (tests/e2e/auth/sign-in.spec.ts)
Security note: Use a unique password only for testing. Never reuse production passwords.

System Variables

NODE_ENV
string
The Node.js environment. Automatically set by Next.js and hosting platforms.Values:
  • development - Local dev server (pnpm dev)
  • production - Production build (pnpm build && pnpm start)
  • test - Testing environment (set by testing frameworks)
Used in:
  • Auth callback redirects (src/app/auth/callback/route.ts)
  • PostHog debug mode (src/components/posthog-provider.tsx)
  • Conditional behavior based on environment
You don’t need to set this manually. Next.js and hosting platforms handle it automatically.
CI
boolean
Indicates whether the code is running in a CI/CD environment.Used in:
  • Playwright configuration (playwright.config.ts)
    • Enables forbidOnly (fails if test.only() is left in code)
    • Sets retries: 2 (retries flaky tests twice)
    • Sets workers: 1 (sequential test execution)
    • Disables reuseExistingServer (always starts fresh server)
Automatically set by CI platforms like GitHub Actions, GitLab CI, CircleCI, etc. You don’t need to set this manually.

Setup Guide

Local Development Setup

1

Create .env.local file

Copy the template below and replace placeholders with your actual values:
.env.local
# Site
NEXT_PUBLIC_SITE_URL=http://localhost:3000

# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SECRET_KEY=sbp_1234567890abcdef...

# Stripe
STRIPE_SECRET_KEY=sk_test_51A...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51A...

# Stripe Plans (optional - can hardcode in plans.ts instead)
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY=price_...
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY=price_...

# Stripe Customer Portal (optional)
STRIPE_CUSTOMER_PORTAL_CONFIG_ID=bpc_...

# Polar
POLAR_ACCESS_TOKEN=polar_at_...
POLAR_WEBHOOK_SECRET=polar_wh_...
POLAR_SANDBOX=true
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLY=prod_monthly_...
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY=prod_yearly_...

# PostHog (optional)
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Restart your dev server (pnpm dev) after creating or modifying .env.local.
2

Add .env.local to .gitignore

Ensure .env.local is in your .gitignore file:
.gitignore
# Environment variables
.env.local
.env.test
.env*.local
Never commit .env.local to version control. It contains sensitive credentials.
3

Create .env.test for testing

If you plan to run E2E tests, create .env.test:
.env.test
# Test user credentials
TEST_USER_EMAIL=[email protected]
TEST_USER_PASSWORD=your_test_password

# Same Supabase credentials as .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Production Setup (Vercel)

1

Deploy to Vercel

  1. Push your code to GitHub
  2. Import repository in Vercel
  3. Vercel will detect Next.js automatically
2

Add environment variables

In Vercel Dashboard → Your Project → Settings → Environment Variables, add:
# Site
NEXT_PUBLIC_SITE_URL=https://yourdomain.com

# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGci...
SUPABASE_SECRET_KEY=sbp_123...

# Stripe (use live keys, not test keys)
STRIPE_SECRET_KEY=sk_live_51A...
STRIPE_WEBHOOK_SECRET=whsec_... (from production webhook)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_51A...

# Stripe Plans
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY=price_...
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY=price_...

# Polar
POLAR_ACCESS_TOKEN=polar_at_...
POLAR_WEBHOOK_SECRET=polar_wh_...
POLAR_SANDBOX=false
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLY=prod_monthly_...
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY=prod_yearly_...

# PostHog (optional)
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Set variables for all environments (Production, Preview, Development) or scope them individually.
3

Update Supabase URLs

In Supabase Dashboard → Authentication → URL Configuration:
  • Site URL: https://yourdomain.com
  • Redirect URLs:
    • https://yourdomain.com/auth/callback
    • https://*.vercel.app/auth/callback (for preview deployments)
Vercel preview deployments get unique URLs. Use wildcard patterns to allow preview authentication.
4

Set up production Stripe webhook

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click “Add endpoint”
  3. Endpoint URL: https://yourdomain.com/api/webhooks/stripe
  4. Select events:
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.paid
    • invoice.payment_failed
  5. Copy signing secret and add to Vercel as STRIPE_WEBHOOK_SECRET
Production webhook secret is different from local Stripe CLI secret. Make sure to update it in Vercel.
5

Set up production Polar webhook

  1. Polar Dashboard → Settings → Webhooks → Add endpoint.
  2. Endpoint URL: https://yourdomain.com/api/webhooks/polar.
  3. Subscribe to subscription + order events.
  4. Copy the signing secret into Vercel as POLAR_WEBHOOK_SECRET.
  5. Ensure POLAR_SANDBOX=false so /api/checkout and the webhook use live APIs.
Every new Polar webhook endpoint provides a fresh secret. Update your environment variable whenever you recreate the endpoint (e.g., new ngrok URL, new production domain).
6

Redeploy

After adding environment variables, redeploy your app:
git commit --allow-empty -m "Trigger redeploy"
git push
Or click “Redeploy” in Vercel Dashboard.
For a complete deployment checklist (build command, server config, secrets), refer to the Vercel deployment guide.

Troubleshooting

Symptoms: Variables are undefined in code, features not working.Causes:
  1. Server not restarted after changing .env.local
  2. Variable name typo
  3. Trying to access server-only variable in client code
  4. .env.local file in wrong directory
Fix:
  1. Restart dev server: Stop (Ctrl+C) and restart (pnpm dev)
  2. Check spelling: Variable names are case-sensitive
  3. Check client/server: Only NEXT_PUBLIC_* variables work in browser
  4. Verify location: .env.local must be in project root (same directory as package.json)
# Verify variables are loaded
pnpm dev

# You should see output like:
# Loaded env from /path/to/project/.env.local
Cause: Supabase environment variables not set or server not restarted.Fix:
  1. Check .env.local has both NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
  2. Restart dev server: pnpm dev
  3. Verify no typos in variable names (common: ANON_KEY vs PUBLISHABLE_KEY)
Sabo uses PUBLISHABLE_KEY not ANON_KEY:
# ✅ Correct
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGci...

# ❌ Wrong (common mistake)
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
Cause: Incorrect STRIPE_WEBHOOK_SECRET or webhook not from Stripe.Fix:Local development:
  1. Ensure Stripe CLI is running: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  2. Copy the whsec_... secret from terminal output
  3. Update STRIPE_WEBHOOK_SECRET in .env.local
  4. Restart server
Production:
  1. Verify webhook endpoint URL matches exactly: https://yourdomain.com/api/webhooks/stripe
  2. Check webhook signing secret in Stripe Dashboard → Webhooks → [Your endpoint] → Signing secret
  3. Update STRIPE_WEBHOOK_SECRET in Vercel
  4. Redeploy
Test webhook locally: stripe trigger checkout.session.completed
Cause: POLAR_WEBHOOK_SECRET doesn’t match the endpoint configured in Polar Dashboard, or the webhook still points to an old URL (common when ngrok restarts).Fix:
  1. Reconfirm the webhook endpoint URL in Polar Dashboard → Settings → Webhooks.
  2. Copy the latest signing secret and update POLAR_WEBHOOK_SECRET (.env.local for dev, hosting env for prod).
  3. Restart the dev server or redeploy.
  4. Trigger a sandbox checkout to emit subscription.created / order.paid events and ensure the terminal logs show 200 OK.
If you rotate ngrok URLs frequently, keep the Polar dashboard tab open so you can update the endpoint + secret immediately after restarting ngrok.
Cause: NEXT_PUBLIC_SITE_URL doesn’t match Supabase configuration.Fix:
  1. Check NEXT_PUBLIC_SITE_URL in .env.local:
    # Local
    NEXT_PUBLIC_SITE_URL=http://localhost:3000
    
    # Production
    NEXT_PUBLIC_SITE_URL=https://yourdomain.com
    
  2. Ensure it matches Supabase Dashboard → Authentication → URL Configuration:
    • Site URL must match NEXT_PUBLIC_SITE_URL exactly
    • Redirect URLs must include {NEXT_PUBLIC_SITE_URL}/auth/callback
  3. No trailing slash in URL
Common mistakes:
  • http://localhost:3000/ ❌ (trailing slash)
  • http://localhost:3000
Cause: Missing or incorrect PostHog environment variables.Fix:
  1. Verify both variables are set:
    NEXT_PUBLIC_POSTHOG_KEY=phc_...
    NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
    
  2. Check PostHog key format starts with phc_
  3. Restart dev server
  4. Open browser console and look for [PostHog] debug messages (in development mode)
  5. Disable ad blockers (they often block PostHog)
PostHog only activates when both NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST are set. If either is missing, PostHog is disabled.
Cause: Missing test credentials or incorrect .env.test setup.Fix:
  1. Create .env.test in project root:
    TEST_USER_EMAIL=[email protected]
    TEST_USER_PASSWORD=your_test_password
    NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
    NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGci...
    
  2. Create test user in Supabase:
    • Go to Supabase Dashboard → Authentication → Users
    • Add user manually or via SQL
  3. Verify test user exists and email is confirmed
  4. Run tests: pnpm test:e2e
Use a dedicated test user, not a real user account. Test data may be created/deleted during tests.

Security Best Practices

What to do:
  • Add .env.local, .env.test, .env*.local to .gitignore
  • Use environment variables, not hardcoded secrets
  • Review commits before pushing to ensure no secrets leaked
If you accidentally commit secrets:
  1. Rotate all compromised keys immediately
  2. Remove from git history: git filter-branch or use BFG Repo-Cleaner
  3. Force push to remote (if safe to do so)
Why: Prevents test data from mixing with production data and limits blast radius if dev keys are compromised.Setup:
  • Stripe: Use test keys (sk_test_, pk_test_) in development
  • Supabase: Use separate projects for dev and prod (recommended)
  • PostHog: Use separate projects or test mode
Verification:
# Development (.env.local)
STRIPE_SECRET_KEY=sk_test_51A...

# Production (Vercel)
STRIPE_SECRET_KEY=sk_live_51A...
Server-only variables (never prefix with NEXT_PUBLIC_):
  • SUPABASE_SECRET_KEY - Bypasses Row Level Security
  • STRIPE_SECRET_KEY - Full Stripe account access
  • STRIPE_WEBHOOK_SECRET - Webhook verification
  • POLAR_ACCESS_TOKEN - Full Polar API access
  • POLAR_WEBHOOK_SECRET - Polar webhook verification
Why: Client-side code is visible to users. Anyone can view NEXT_PUBLIC_* variables in browser DevTools.Check your code:
// ❌ Bad - trying to access server-only variable in client
"use client";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // undefined!

// ✅ Good - server-only variable in API route
export async function POST() {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
}
Recommended schedule:
  • Production secrets: Every 90 days
  • After team member departure: Immediately
  • After suspected breach: Immediately
How to rotate:Supabase:
  1. Generate new secret key in Supabase Dashboard → Project Settings → API
  2. Update SUPABASE_SECRET_KEY in hosting platform
  3. Redeploy
  4. Revoke old key
Stripe:
  1. Create new secret key in Stripe Dashboard → Developers → API keys
  2. Update STRIPE_SECRET_KEY in hosting platform
  3. Redeploy
  4. Delete old key from Stripe Dashboard
Why: Prevents production webhooks from hitting development servers and vice versa.Setup:
# Development
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Stripe webhook points to local with Stripe CLI forwarding

# Staging
NEXT_PUBLIC_SITE_URL=https://staging.yourdomain.com
# Separate Stripe webhook endpoint

# Production
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
# Production Stripe webhook endpoint

Quick Reference

All Variables Summary

VariableAccessRequired?Where Used
NEXT_PUBLIC_SITE_URLPublicYesOAuth redirects, email links, Stripe portal
NEXT_PUBLIC_SUPABASE_URLPublicYesAll Supabase clients
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYPublicYesAll Supabase clients
SUPABASE_SECRET_KEYSecretYesService client, webhooks
STRIPE_SECRET_KEYSecretYesStripe API, checkout, portal
STRIPE_WEBHOOK_SECRETSecretYesWebhook verification
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYPublicYesClient-side Stripe
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLYPublicOptionalPricing page
NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLYPublicOptionalPricing page
STRIPE_CUSTOMER_PORTAL_CONFIG_IDSecretOptionalCustomer portal customization
POLAR_ACCESS_TOKENSecretYesPolar checkout, portal, webhooks
POLAR_WEBHOOK_SECRETSecretYesPolar webhook verification
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLYPublicOptionalPolar plan lookup + checkout
NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLYPublicOptionalPolar plan lookup + checkout
POLAR_SANDBOXSecretOptionalToggle sandbox vs production Polar API
NEXT_PUBLIC_POSTHOG_KEYPublicOptionalPostHog analytics
NEXT_PUBLIC_POSTHOG_HOSTPublicOptionalPostHog analytics
TEST_USER_EMAILSecretOptionalPlaywright tests
TEST_USER_PASSWORDSecretOptionalPlaywright tests
NODE_ENVSystemAutoEnvironment detection
CISystemAutoCI/CD behavior