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_..."
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: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: