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_..."
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_..."
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)
Get your key from Arcjet
Webhooks (Svix)
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)
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
- Add the variable to the relevant
.env file
- 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,
},
});
- Import from
@/env in your code
- 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:
| 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:
cd apps/app && vercel link && cd ../..
cd apps/web && vercel link && cd ../..
Then sync everything: