> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getsabo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Variables Reference

> Complete reference of all required and optional environment variables for configuring Sabo, organized by feature with usage examples and setup instructions.

<Info>
  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.
</Info>

## 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.)

<Warning>
  Never commit `.env.local` or `.env.test` to version control. These files contain sensitive credentials and should be added to `.gitignore`.
</Warning>

### 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)

<Note>
  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`.
</Note>

***

## Required Variables

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

### Core Application

<ParamField path="NEXT_PUBLIC_SITE_URL" type="string" required>
  The base URL of your application. Used for generating absolute URLs, OAuth redirects, and email links.

  **Local development:**

  ```bash theme={null}
  NEXT_PUBLIC_SITE_URL=http://localhost:3000
  ```

  **Production:**

  ```bash theme={null}
  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`)

  <Warning>
    Must match the URL configured in Supabase Authentication → URL Configuration. Mismatches will cause OAuth and email verification to fail.
  </Warning>
</ParamField>

### Supabase (Authentication & Database)

<ParamField path="NEXT_PUBLIC_SUPABASE_URL" type="string" required>
  Your Supabase project URL. Find this in your Supabase Dashboard → Project Settings → API.

  **Format:**

  ```bash theme={null}
  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`)

  <Tip>
    Each Supabase project has a unique subdomain. Copy the entire URL including `https://`.
  </Tip>
</ParamField>

<ParamField path="NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY" type="string" required>
  Your Supabase project's anonymous key (also called "anon key"). This is safe to expose to the browser.

  **Format:**

  ```bash theme={null}
  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`

  <Check>
    This key is safe to expose in client-side code. It respects Row Level Security (RLS) policies.
  </Check>
</ParamField>

<ParamField path="SUPABASE_SECRET_KEY" type="string" required>
  Your Supabase project's secret key. This bypasses Row Level Security and should **never** be exposed to the browser.

  **Format:**

  ```bash theme={null}
  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`

  <Warning>
    **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.
  </Warning>
</ParamField>

<Tip>
  Need deep dives on Supabase tables, clients, and auth flows? Review <a href="/core-features/database-supabase">Database with Supabase</a> and <a href="/core-features/auth-with-supabase">Auth with Supabase</a> for end-to-end examples.
</Tip>

### Stripe (Payments)

<ParamField path="STRIPE_SECRET_KEY" type="string" required>
  Your Stripe secret API key. Used to create checkout sessions, manage subscriptions, and access the Stripe API.

  **Test mode:**

  ```bash theme={null}
  STRIPE_SECRET_KEY=sk_test_51A...
  ```

  **Production mode:**

  ```bash theme={null}
  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

  <Warning>
    **Server-only variable.** Keep this secret! It provides full access to your Stripe account.
  </Warning>
</ParamField>

<ParamField path="STRIPE_WEBHOOK_SECRET" type="string" required>
  The webhook signing secret for verifying Stripe webhook events. Ensures webhook requests actually come from Stripe.

  **Local development (Stripe CLI):**

  ```bash theme={null}
  STRIPE_WEBHOOK_SECRET=whsec_...
  ```

  **Production:**

  ```bash theme={null}
  STRIPE_WEBHOOK_SECRET=whsec_...
  ```

  **Used in:**

  * Webhook signature verification (`src/app/api/webhooks/stripe/route.ts`)

  **How to get:**

  <Tabs>
    <Tab title="Local Development">
      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
    </Tab>

    <Tab title="Production">
      1. Go to Stripe Dashboard → Developers → Webhooks
      2. Click "Add endpoint"
      3. Enter your webhook URL: `https://yourdomain.com/api/webhooks/stripe`
      4. Select events (see Stripe documentation for full list)
      5. Copy the signing secret
    </Tab>
  </Tabs>

  <Check>
    Stripe CLI automatically forwards webhook events to your local server and provides a test signing secret.
  </Check>
</ParamField>

<ParamField path="NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" type="string" required>
  Your Stripe publishable API key. Safe to expose to the browser. Used for Stripe Checkout and payment forms.

  **Test mode:**

  ```bash theme={null}
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51A...
  ```

  **Production mode:**

  ```bash theme={null}
  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

  <Tip>
    Use test keys for development and live keys for production. Test keys start with `sk_test_` and `pk_test_`.
  </Tip>
</ParamField>

<Tip>
  Once your Stripe variables are in place, follow the dedicated <a href="/core-features/payments-with-stripe">Payments with Stripe</a> guide to configure products, plans, and webhooks.
</Tip>

### Polar (Payments)

<ParamField path="POLAR_ACCESS_TOKEN" type="string" required>
  Server-side token used by Polar Checkout, Customer Portal, and webhook helpers.

  **Format:**

  ```bash theme={null}
  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.

  <Warning>
    Treat this exactly like a Stripe secret key. Keep it server-only and rotate it if leaked.
  </Warning>
</ParamField>

<ParamField path="POLAR_WEBHOOK_SECRET" type="string" required>
  Signing secret used to verify Polar webhook payloads.

  **Format:**

  ```bash theme={null}
  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`

  <Check>
    Whenever you recreate the webhook endpoint (new ngrok URL or production domain), update this secret and restart the server.
  </Check>
</ParamField>

<Tip>
  Follow the <a href="/core-features/payments/payments-with-polar">Payments with Polar</a> guide to finish product IDs, checkout metadata, and webhook setup.
</Tip>

***

## Optional Variables

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

### Stripe (Advanced)

<ParamField path="NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY" type="string" optional>
  The Stripe Price ID for your Pro plan monthly subscription.

  **Format:**

  ```bash theme={null}
  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_`)

  <Note>
    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.
  </Note>
</ParamField>

<ParamField path="NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY" type="string" optional>
  The Stripe Price ID for your Pro plan yearly subscription.

  **Format:**

  ```bash theme={null}
  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.
</ParamField>

<ParamField path="STRIPE_CUSTOMER_PORTAL_CONFIG_ID" type="string" optional>
  The configuration ID for your Stripe Customer Portal. Allows customization of which features customers can manage.

  **Format:**

  ```bash theme={null}
  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

  <Tip>
    If not set, Stripe uses your account's default Customer Portal configuration.
  </Tip>
</ParamField>

### Polar (Advanced)

<ParamField path="NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLY" type="string" optional>
  Polar product ID for the monthly Pro tier. Rendered in the pricing UI and appended to `/api/checkout`.

  **Format:**

  ```bash theme={null}
  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`)

  <Tip>
    You can hardcode IDs directly in `plans.ts`, but environment variables make it easier to swap products per environment.
  </Tip>
</ParamField>

<ParamField path="NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY" type="string" optional>
  Polar product ID for the yearly Pro tier.

  **Format:**

  ```bash theme={null}
  NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY=prod_yearly_123
  ```

  **Used in:** `src/lib/payments/plans.ts`, pricing toggle, webhook plan lookup helper.
</ParamField>

<ParamField path="POLAR_SANDBOX" type="boolean" optional>
  Controls whether helper utilities call the sandbox or production Polar API (`https://sandbox-api.polar.sh` vs `https://api.polar.sh`).

  ```bash theme={null}
  # 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`

  <Note>
    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](/core-features/payments/payments-with-polar) guide for details.
  </Note>

  <Warning>
    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.
  </Warning>
</ParamField>

### PostHog (Analytics)

<ParamField path="NEXT_PUBLIC_POSTHOG_KEY" type="string" optional>
  Your PostHog project API key. Required to enable PostHog analytics.

  **Format:**

  ```bash theme={null}
  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

  <Note>
    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.
  </Note>
</ParamField>

<ParamField path="NEXT_PUBLIC_POSTHOG_HOST" type="string" optional>
  The PostHog host URL. Depends on your PostHog hosting choice.

  **PostHog Cloud (US):**

  ```bash theme={null}
  NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
  ```

  **PostHog Cloud (EU):**

  ```bash theme={null}
  NEXT_PUBLIC_POSTHOG_HOST=https://eu.posthog.com
  ```

  **Self-hosted:**

  ```bash theme={null}
  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`)

  <Tip>
    Choose the region closest to your users for better performance. EU region is GDPR-compliant by default.
  </Tip>
</ParamField>

<Tip>
  For implementation patterns (client/provider setup, feature flags, server events), see the <a href="/core-features/analytics-posthog">Analytics with PostHog</a> guide.
</Tip>

### Testing (Playwright)

<ParamField path="TEST_USER_EMAIL" type="string" optional>
  Email address of a test user for E2E authentication tests.

  **Format:**

  ```bash theme={null}
  TEST_USER_EMAIL=test@example.com
  ```

  **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

  <Warning>
    Only set in `.env.test`, not `.env.local`. Keep test credentials separate from development credentials.
  </Warning>
</ParamField>

<ParamField path="TEST_USER_PASSWORD" type="string" optional>
  Password for the test user account.

  **Format:**

  ```bash theme={null}
  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.
</ParamField>

### System Variables

<ParamField path="NODE_ENV" type="string" automatic>
  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

  <Note>
    You don't need to set this manually. Next.js and hosting platforms handle it automatically.
  </Note>
</ParamField>

<ParamField path="CI" type="boolean" automatic>
  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)

  <Note>
    Automatically set by CI platforms like GitHub Actions, GitLab CI, CircleCI, etc. You don't need to set this manually.
  </Note>
</ParamField>

***

## Setup Guide

### Local Development Setup

<Steps>
  <Step title="Create .env.local file">
    Copy the template below and replace placeholders with your actual values:

    ```bash .env.local theme={null}
    # 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
    ```

    <Check>
      Restart your dev server (`pnpm dev`) after creating or modifying `.env.local`.
    </Check>
  </Step>

  <Step title="Add .env.local to .gitignore">
    Ensure `.env.local` is in your `.gitignore` file:

    ```bash .gitignore theme={null}
    # Environment variables
    .env.local
    .env.test
    .env*.local
    ```

    <Warning>
      Never commit `.env.local` to version control. It contains sensitive credentials.
    </Warning>
  </Step>

  <Step title="Create .env.test for testing">
    If you plan to run E2E tests, create `.env.test`:

    ```bash .env.test theme={null}
    # Test user credentials
    TEST_USER_EMAIL=test@example.com
    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...
    ```
  </Step>
</Steps>

### Production Setup (Vercel)

<Steps>
  <Step title="Deploy to Vercel">
    1. Push your code to GitHub
    2. Import repository in [Vercel](https://vercel.com)
    3. Vercel will detect Next.js automatically
  </Step>

  <Step title="Add environment variables">
    In Vercel Dashboard → Your Project → Settings → Environment Variables, add:

    ```bash theme={null}
    # 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
    ```

    <Tip>
      Set variables for all environments (Production, Preview, Development) or scope them individually.
    </Tip>
  </Step>

  <Step title="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)

    <Check>
      Vercel preview deployments get unique URLs. Use wildcard patterns to allow preview authentication.
    </Check>
  </Step>

  <Step title="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`

    <Warning>
      Production webhook secret is different from local Stripe CLI secret. Make sure to update it in Vercel.
    </Warning>
  </Step>

  <Step title="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.

    <Warning>
      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).
    </Warning>
  </Step>

  <Step title="Redeploy">
    After adding environment variables, redeploy your app:

    ```bash theme={null}
    git commit --allow-empty -m "Trigger redeploy"
    git push
    ```

    Or click "Redeploy" in Vercel Dashboard.
  </Step>
</Steps>

<Info>
  For a complete deployment checklist (build command, server config, secrets), refer to the <a href="/deployment/vercel">Vercel deployment guide</a>.
</Info>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Environment variables not working">
    **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`)

    ```bash theme={null}
    # Verify variables are loaded
    pnpm dev

    # You should see output like:
    # Loaded env from /path/to/project/.env.local
    ```
  </Accordion>

  <Accordion title="'NEXT_PUBLIC_SUPABASE_URL is not defined' error">
    **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`:**

    ```bash theme={null}
    # ✅ Correct
    NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=eyJhbGci...

    # ❌ Wrong (common mistake)
    NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
    ```
  </Accordion>

  <Accordion title="Stripe webhook signature verification failed">
    **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

    <Tip>
      Test webhook locally: `stripe trigger checkout.session.completed`
    </Tip>
  </Accordion>

  <Accordion title="Polar webhook signature verification failed">
    **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`.

    <Tip>
      If you rotate ngrok URLs frequently, keep the Polar dashboard tab open so you can update the endpoint + secret immediately after restarting ngrok.
    </Tip>
  </Accordion>

  <Accordion title="OAuth redirect not working">
    **Cause:** `NEXT_PUBLIC_SITE_URL` doesn't match Supabase configuration.

    **Fix:**

    1. Check `NEXT_PUBLIC_SITE_URL` in `.env.local`:
       ```bash theme={null}
       # 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` ✅
  </Accordion>

  <Accordion title="PostHog not tracking events">
    **Cause:** Missing or incorrect PostHog environment variables.

    **Fix:**

    1. Verify both variables are set:
       ```bash theme={null}
       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)

    <Note>
      PostHog only activates when **both** `NEXT_PUBLIC_POSTHOG_KEY` and `NEXT_PUBLIC_POSTHOG_HOST` are set. If either is missing, PostHog is disabled.
    </Note>
  </Accordion>

  <Accordion title="Playwright tests can't authenticate">
    **Cause:** Missing test credentials or incorrect `.env.test` setup.

    **Fix:**

    1. Create `.env.test` in project root:
       ```bash theme={null}
       TEST_USER_EMAIL=test@example.com
       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`

    <Warning>
      Use a dedicated test user, not a real user account. Test data may be created/deleted during tests.
    </Warning>
  </Accordion>
</AccordionGroup>

***

## Security Best Practices

<AccordionGroup>
  <Accordion title="1. Never commit secrets to version control">
    **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)
  </Accordion>

  <Accordion title="2. Use different keys for development and production">
    **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:**

    ```bash theme={null}
    # Development (.env.local)
    STRIPE_SECRET_KEY=sk_test_51A...

    # Production (Vercel)
    STRIPE_SECRET_KEY=sk_live_51A...
    ```
  </Accordion>

  <Accordion title="3. Restrict server-only variables">
    **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:**

    ```tsx theme={null}
    // ❌ 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);
    }
    ```
  </Accordion>

  <Accordion title="4. Rotate keys regularly">
    **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
  </Accordion>

  <Accordion title="5. Use environment-specific URLs">
    **Why:** Prevents production webhooks from hitting development servers and vice versa.

    **Setup:**

    ```bash theme={null}
    # 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
    ```
  </Accordion>
</AccordionGroup>

***

## Quick Reference

### All Variables Summary

| Variable                                   | Access | Required? | Where Used                                  |
| ------------------------------------------ | ------ | --------- | ------------------------------------------- |
| `NEXT_PUBLIC_SITE_URL`                     | Public | Yes       | OAuth redirects, email links, Stripe portal |
| `NEXT_PUBLIC_SUPABASE_URL`                 | Public | Yes       | All Supabase clients                        |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`     | Public | Yes       | All Supabase clients                        |
| `SUPABASE_SECRET_KEY`                      | Secret | Yes       | Service client, webhooks                    |
| `STRIPE_SECRET_KEY`                        | Secret | Yes       | Stripe API, checkout, portal                |
| `STRIPE_WEBHOOK_SECRET`                    | Secret | Yes       | Webhook verification                        |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`       | Public | Yes       | Client-side Stripe                          |
| `NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_MONTHLY`  | Public | Optional  | Pricing page                                |
| `NEXT_PUBLIC_STRIPE_PRICE_ID_PRO_YEARLY`   | Public | Optional  | Pricing page                                |
| `STRIPE_CUSTOMER_PORTAL_CONFIG_ID`         | Secret | Optional  | Customer portal customization               |
| `POLAR_ACCESS_TOKEN`                       | Secret | Yes       | Polar checkout, portal, webhooks            |
| `POLAR_WEBHOOK_SECRET`                     | Secret | Yes       | Polar webhook verification                  |
| `NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_MONTHLY` | Public | Optional  | Polar plan lookup + checkout                |
| `NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO_YEARLY`  | Public | Optional  | Polar plan lookup + checkout                |
| `POLAR_SANDBOX`                            | Secret | Optional  | Toggle sandbox vs production Polar API      |
| `NEXT_PUBLIC_POSTHOG_KEY`                  | Public | Optional  | PostHog analytics                           |
| `NEXT_PUBLIC_POSTHOG_HOST`                 | Public | Optional  | PostHog analytics                           |
| `TEST_USER_EMAIL`                          | Secret | Optional  | Playwright tests                            |
| `TEST_USER_PASSWORD`                       | Secret | Optional  | Playwright tests                            |
| `NODE_ENV`                                 | System | Auto      | Environment detection                       |
| `CI`                                       | System | Auto      | CI/CD behavior                              |

***
