Skip to main content
mf² uses environment variables for configuration. This page covers the variables needed to get running and the optional ones that add features.
All integration environment variables are optional. Features like Stripe, PostHog, BaseHub CMS, email, and feature flags will gracefully degrade when their env vars are missing — returning safe defaults instead of crashing. The only truly required variable is your Convex deployment URL.

Quick start

To get mf² running locally, configure these three services.

1. Authentication (Clerk)

Add to your app’s environment (e.g. Vercel dashboard or local .env.local):
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
CLERK_SECRET_KEY="sk_test_..."
1
Create an application in the Clerk Dashboard
2
Go to API Keys
3
Copy the Publishable key (starts with pk_) and Secret key (starts with sk_)

2. Backend (Convex)

Running bunx convex dev generates the deployment URL and writes it to .env.local automatically.
NEXT_PUBLIC_CONVEX_URL="https://your-project.convex.cloud"

3. Payments (Stripe)

STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
1
Get your keys from the Stripe Dashboard
2
For local webhooks, run:
stripe listen --forward-to localhost:3000/api/webhook/stripe
The CLI prints a signing secret to use as STRIPE_WEBHOOK_SECRET.
You can now run bun run dev and the app works with auth, backend, and payments.

Additional features

Add these as needed.

Email (Resend)

RESEND_TOKEN="re_..."
RESEND_FROM="noreply@yourdomain.com"
Get your API key from Resend

Analytics (PostHog)

NEXT_PUBLIC_POSTHOG_KEY="phc_..."
NEXT_PUBLIC_POSTHOG_HOST="https://app.posthog.com"
Get your keys from PostHog

Analytics (Google)

NEXT_PUBLIC_GA_MEASUREMENT_ID="G-..."
Create a GA4 property

Error tracking (Sentry)

SENTRY_DSN="https://..."
SENTRY_ORG="your-org"
SENTRY_PROJECT="your-project"
Get your DSN from Sentry

Observability (BetterStack)

BETTERSTACK_API_KEY="..."
BETTERSTACK_URL="..."
Get your API key from BetterStack

Security (Arcjet)

ARCJET_KEY="ajkey_..."
Get your key from Arcjet

Webhooks (Svix)

SVIX_TOKEN="..."
Get your token from Svix

Notifications (Knock)

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

Collaboration (Liveblocks)

LIVEBLOCKS_SECRET="sk_..."
Get your secret from Liveblocks

CMS (BaseHub)

BASEHUB_TOKEN="bshb_..."
Get your token from BaseHub

AI (Vercel AI Gateway)

AI_GATEWAY_API_KEY="..."
AI_GATEWAY_URL="..."

Clerk Webhooks

CLERK_WEBHOOK_SECRET="whsec_..."
See Convex Provider — 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:
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    CLERK_SECRET_KEY: z.string().min(1),
    STRIPE_SECRET_KEY: z.string().min(1),
    STRIPE_WEBHOOK_SECRET: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
    NEXT_PUBLIC_CONVEX_URL: z.string().url(),
  },
  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,
  },
});
If a required variable is missing or malformed, the build fails with a clear error message. Import env instead of accessing process.env directly:
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.
Be specific with validation. If a vendor secret starts with sk_, validate it as z.string().min(1).startsWith('sk_'). This catches misconfiguration at build time instead of runtime.

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:
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,
  },
});
  1. Import from @/env in your code
  2. Add it to .env.example so teammates know it exists

Env Scripts

mf² includes a Bun script for managing environment files across the monorepo:
bun run env:init     # Create .env.local + .env.production from .env.example
bun run env:check    # Validate all env files have required keys filled in
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. Users create their own .env.local as needed. env:init scans for .env.example files in apps/ and packages/, then creates both .env.local (for development) and .env.production (for production) if they don’t exist. env:check compares each .env.example against its .env.local and .env.production, reporting any missing or empty keys grouped by app. env:push syncs variables to your deployment platforms:
SourceVercel targetConvex target
.env.localdevelopment + previewdev deployment
.env.productionproductionprod 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:
cd apps/app && vercel link && cd ../..
cd apps/web && vercel link && cd ../..
Then sync everything:
bun run env:push