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.

Quick start

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

1. Authentication (Clerk)

Add to apps/app/.env.local and apps/web/.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: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