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

# Environment Variables

> API keys and service connections for mf².

mf² uses environment variables for configuration. This page covers the keys that unlock the core services and the ones that add extra features.

<Tip>
  All environment variables are **optional**. A blank or missing value simply disables that integration: features like Stripe, PostHog, BaseHub CMS, email, and feature flags gracefully degrade instead of crashing. Nothing is required just to boot; validation only fails when a variable is set to a malformed value.
</Tip>

## Quick start

These three services unlock the core of mf²: sign-in (Clerk), data (Convex), and payments (Stripe). None of them are required just to run `bun run dev`.

### 1. Authentication (Clerk)

Add to your app's environment (e.g. Vercel dashboard or local `.env.local`):

```bash theme={null}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
CLERK_SECRET_KEY="sk_test_..."
```

<Steps>
  <Step>
    Create an application in the [Clerk Dashboard](https://dashboard.clerk.com)
  </Step>

  <Step>
    Go to **API Keys**
  </Step>

  <Step>
    Copy the **Publishable key** (starts with `pk_`) and **Secret key** (starts with `sk_`)
  </Step>
</Steps>

### 2. Backend (Convex)

Running `bunx convex dev` generates the deployment URL and writes it to `.env.local` automatically.

```bash theme={null}
NEXT_PUBLIC_CONVEX_URL="https://your-project.convex.cloud"
```

### 3. Payments (Stripe)

```bash theme={null}
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
```

<Steps>
  <Step>
    Get your keys from the [Stripe Dashboard](https://dashboard.stripe.com/apikeys)
  </Step>

  <Step>
    For local webhooks, run:

    ```bash theme={null}
    stripe listen --forward-to localhost:3000/api/webhook/stripe
    ```

    The CLI prints a signing secret to use as `STRIPE_WEBHOOK_SECRET`.
  </Step>
</Steps>

You can now run `bun run dev` and the app works with auth, backend, and payments.

## Additional features

Add these as needed.

### Email (Resend)

```bash theme={null}
RESEND_TOKEN="re_..."
RESEND_FROM="noreply@yourdomain.com"
```

[Get your API key from Resend](https://resend.com/api-keys)

### Analytics (PostHog)

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

[Get your keys from PostHog](https://app.posthog.com/project/settings)

### Analytics (Google)

```bash theme={null}
NEXT_PUBLIC_GA_MEASUREMENT_ID="G-..."
```

[Create a GA4 property](https://analytics.google.com/)

### Error tracking (Sentry)

```bash theme={null}
SENTRY_DSN="https://..."
SENTRY_ORG="your-org"
SENTRY_PROJECT="your-project"
```

[Get your DSN from Sentry](https://sentry.io/)

### Observability (BetterStack)

```bash theme={null}
BETTERSTACK_API_KEY="..."
BETTERSTACK_URL="..."
```

[Get your API key from BetterStack](https://betterstack.com/logs)

### Security (Arcjet)

```bash theme={null}
ARCJET_KEY="ajkey_..."
```

[Get your key from Arcjet](https://app.arcjet.com/)

### Webhooks (Svix)

```bash theme={null}
SVIX_TOKEN="..."
```

[Get your token from Svix](https://dashboard.svix.com/)

### Notifications (Knock)

```bash theme={null}
KNOCK_API_KEY="..."
KNOCK_SECRET_API_KEY="..."
KNOCK_FEED_CHANNEL_ID="..."
NEXT_PUBLIC_KNOCK_API_KEY="..."
NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID="..."
```

[Get your keys from Knock](https://dashboard.knock.app/)

### Collaboration (Liveblocks)

```bash theme={null}
LIVEBLOCKS_SECRET="sk_..."
```

[Get your secret from Liveblocks](https://liveblocks.io/dashboard)

### CMS (BaseHub)

```bash theme={null}
BASEHUB_TOKEN="bshb_..."
```

[Get your token from BaseHub](https://basehub.com/)

### AI (Vercel AI Gateway)

```bash theme={null}
AI_GATEWAY_API_KEY="..."
AI_GATEWAY_URL="..."
```

### Clerk Webhooks

```bash theme={null}
CLERK_WEBHOOK_SECRET="whsec_..."
```

See [Convex Provider: Configure the webhook in Clerk](/packages/convex#configure-the-webhook-in-clerk) for step-by-step setup.

## Type safety

mf² validates environment variables at build time using `@t3-oss/env-nextjs`. Each app has an `env.ts` file that defines the schema:

```typescript theme={null}
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    CLERK_SECRET_KEY: z.string().startsWith('sk_').optional(),
    STRIPE_SECRET_KEY: z.string().startsWith('sk_').optional(),
    STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_').optional(),
  },
  client: {
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().startsWith('pk_').optional(),
    NEXT_PUBLIC_CONVEX_URL: z.string().url().optional(),
  },
  runtimeEnv: {
    CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
    NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
  },
  emptyStringAsUndefined: true,
});
```

Every schema sets `emptyStringAsUndefined`, so a blank value behaves exactly like a missing one: the integration is disabled and validation passes. Validation fails only when a variable is set to a malformed value, such as a Clerk secret that does not start with `sk_`, and the build stops with a clear error message.

Import `env` instead of accessing `process.env` directly:

```typescript theme={null}
import { env } from '@/env';

const stripe = new Stripe(env.STRIPE_SECRET_KEY);
```

Next.js exposes variables prefixed with `NEXT_PUBLIC_` to the browser. Never put secrets in `NEXT_PUBLIC_` variables.

<Tip>
  Be specific with validation. If a vendor secret starts with `sk_`, validate it as `z.string().startsWith('sk_').optional()`. This catches misconfiguration at build time instead of runtime.
</Tip>

## Adding a new variable

1. Add the variable to the relevant `.env` file
2. Add validation to the `server` or `client` object in the app's `env.ts` file

Example:

```typescript theme={null}
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    MY_NEW_SECRET: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_MY_VALUE: z.string().optional(),
  },
  runtimeEnv: {
    MY_NEW_SECRET: process.env.MY_NEW_SECRET,
    NEXT_PUBLIC_MY_VALUE: process.env.NEXT_PUBLIC_MY_VALUE,
  },
  emptyStringAsUndefined: true,
});
```

Keep a variable required (no `.optional()`) only when the feature cannot work without it; blank values for required keys fail validation.

3. Import from `@/env` in your code
4. Add it to `.env.example` so teammates know it exists

## Env Scripts

mf² includes a Bun script for managing environment files across the monorepo:

```bash theme={null}
bun run env:check    # List env files and keys that are still blank
bun run env:push     # Sync env vars to Vercel and Convex
```

`.env.example` files are the source of truth for which variables each app and package needs. The CLI creates both `.env.local` (for development) and `.env.production` (for production) from each `.env.example` at scaffold time, so there is no init step; you just fill in keys as you need them.

`env:check` compares each `.env.example` against its `.env.local` and `.env.production`, reporting blank or missing keys grouped by app. If an env file is missing entirely (for example after deleting it, or if you cloned the template repo instead of scaffolding), it tells you which `.env.example` to copy to recreate it.

`env:push` syncs variables to your deployment platforms:

| Source            | Vercel target         | Convex target   |
| ----------------- | --------------------- | --------------- |
| `.env.local`      | development + preview | dev deployment  |
| `.env.production` | production            | prod deployment |

The script filters automatically: `NEXT_PUBLIC_*` vars skip Convex (client-side only), `CONVEX_DEPLOYMENT` and `VERCEL_*` vars are skipped (managed by platforms), and empty or localhost values are ignored.

Before pushing, link each app to its Vercel project:

```bash theme={null}
cd apps/app && vercel link && cd ../..
cd apps/web && vercel link && cd ../..
```

Then sync everything:

```bash theme={null}
bun run env:push
```
