Reference for every environment variable this Next.js app reads, where it's
validated, and how it affects behavior across local dev, testnet, and
mainnet. The authoritative schema lives in src/lib/env.ts; this doc is a
narrative companion to the table in the root README.md.
cp .env.example .env.local
# edit .env.local with real values, then:
pnpm run devEvery variable is optional locally. Leaving .env.local empty (or not
creating it at all) still works — next dev and pnpm test both run
against in-repo mocks (/api/auth/login, /api/wallets, etc.).
These are inlined into the browser bundle at build time. Never put secrets
in a NEXT_PUBLIC_* variable.
NEXT_PUBLIC_API_URL— primary backend base URL. Read directly insrc/app/api/auth/login/route.tsto decide whether to proxy to a real backend or fall back to the mock login response, and insrc/lib/api/config.ts::getApiBaseUrl()as the first candidate for all other API calls (e.g.useWallets,GET /api/requests/today, andPOST /api/transactionsfor the wallet "Send" flow).NEXT_PUBLIC_MUX_API_URL— second candidate in the samegetApiBaseUrl()fallback chain; defaults tohttps://api.muxprotocol.comwhen nothing else is set. PredatesNEXT_PUBLIC_API_URLand is kept for older deploy configs.NEXT_PUBLIC_API_BASE— third and final candidate in the fallback chain, for deploys that used this older name.NEXT_PUBLIC_APP_URL— this app's own public URL; defaults tohttp://localhost:3000.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID— only relevant if WalletConnect-based wallet flows are enabled.
There is intentionally no client-visible Mux API key. A project API key
is a real credential, and anything under NEXT_PUBLIC_* is inlined into
the browser bundle for every visitor to read — see #636. ApiContext.tsx
(a client component) never reads MUX_API_KEY/MUX_API_SECRET; it only
constructs an unauthenticated client that talks to this app's own
same-origin /api/* routes.
These never reach the browser and are safe for secrets.
MUX_API_KEY/MUX_API_SECRET— read bygetUpstreamAuthHeaders()insrc/lib/api/config.tsand attached (x-api-key/x-api-secret) to every upstream request a Next.js API route makes to the Mux backend. Only ever read insidesrc/app/api/**route handlers or other server-only modules — never importgetApiKey()/getApiSecret()from a client component.MUX_BACKEND_URL— server-only base URL ofmux-backend, read bygetBackendApiBaseUrl()insrc/lib/api/config.ts./api/spending-limitsproxiesGET/PUThere (forwarding the server API key and any callerAuthorizationheader) so spending limits and the realtodayUsagecounter live in the backend, not the frontend process. No default: when unset the route responds503 { error: "Spending limits backend is not configured" }instead of returning a fabricated figure. The/api/demo/spending-limitsroute needs no backend — it derives itstodayUsagefrom the mock transaction store (computeTodayUsage()insrc/lib/spending-limits/todayUsage.ts).
NODE_ENV— standard Next.js variable. Gates verbose console logging in the analytics/tracking hooks (useAnalytics.ts,useAnalyticsMetrics.ts,useAnalyticsTracking.ts,recoveryAnalyticsTracking.ts,spendingLimitsTracking.ts) outside ofproduction, makesvalidateEnv()insrc/lib/env.tsthrow (instead of warn) on missing required vars when set toproduction, and controls whethergetEnv()merges in documented defaults (see "Production defaults" below — it only does so whenNODE_ENV=production).
getEnv() merges each var's documented defaultValue (from the schema
in src/lib/env.ts) into whatever is set, but only when
NODE_ENV=production. Concretely: if a production deploy forgets to set
NEXT_PUBLIC_API_URL/NEXT_PUBLIC_MUX_API_URL, it now resolves to the
documented default https://api.muxprotocol.com instead of silently
falling through every API route's mock branch (#637). Local dev and test
runs are untouched — NODE_ENV isn't production, so leaving vars unset
still exercises the in-repo mocks described throughout this doc.
Two independent things decide "which network" a request is scoped to:
-
Which backend —
NEXT_PUBLIC_API_URL(or its aliases) points this app at a specific Mux backend:Environment NEXT_PUBLIC_API_URLexampleLocal dev (mocked) (unset) Testnet / staging https://testnet-api.muxprotocol.comMainnet / production https://api.muxprotocol.com -
Which network within that backend — the in-app Testnet/Mainnet switcher in the top nav (
NetworkContext,src/context/NetworkContext.tsx, persisted tolocalStorageundermux_network).useWallets({ network })sends this as a?network=query param on/api/wallets, so the backend itself scopes the response to one network — wallets are not additionally re-filtered client-side. (An earlier version of the wallets page did also run a second, independent client-side "all/testnet/mainnet" filter on top of that already-scoped fetch, which could show a false "no wallets on this network" empty state whenever it disagreed with the in-app switcher. That double-filtering has been removed — seesrc/app/dashboard/wallets/page.tsx.)
The wallet rows themselves also carry a per-wallet network field
("testnet" | "mainnet", see src/types/wallet.ts) that both the
backend proxy and the mock fallback in /api/wallets use to honor that
query param.
/api/auth/login, /api/auth/refresh, /api/wallets, and
/api/wallets/[id] fall back to in-repo mock responses (fake wallets, a
hardcoded mock bearer/refresh token) whenever no backend URL is
configured — that's what lets pnpm run dev, CI, and the /demo routes
run with no live backend. isMockFallbackAllowed()
(src/lib/api/config.ts) disables that fallback whenever
NODE_ENV=production: those routes return 503 backend_unavailable
instead. This matters because the mock fallback accepts a hardcoded
bearer token (mock-access-token) and refresh token
(mock-refresh-token) as valid — without the guard, a production
deployment that forgot to set NEXT_PUBLIC_API_URL would silently serve
fabricated wallets/analytics and accept those hardcoded tokens as a real
authenticated session.
.github/workflows/ci.yml sets NEXT_PUBLIC_API_URL=https://api.example.com
for the build job purely so next build succeeds without real
credentials. It is a placeholder, not a real environment — do not read it
as evidence of a live mainnet or testnet target.
The e2e-tests job (Playwright) instead sets NEXT_PUBLIC_API_URL="" so
the specs exercise the in-repo mock /api/* routes with no backend. No
job sets MUX_BACKEND_URL, so /api/spending-limits returns 503 in CI
— the e2e specs cover login and wallet flows, not spending limits.
-
.env.localunset entirely →pnpm run devstill boots and login succeeds against the mock/api/auth/loginroute. -
NEXT_PUBLIC_API_URLset to a real backend → login proxies through instead of using the mock. -
NEXT_PUBLIC_APP_URLchanged → any absolute links that use it update accordingly. - Removing a
NEXT_PUBLIC_*var and settingNODE_ENV=productionsurfaces a startup error only for vars markedrequiredinsrc/lib/env.ts(none currently are, by design). -
NODE_ENV=productionwithNEXT_PUBLIC_API_URL(and its aliases) unset →/api/wallets,/api/wallets/[id],/api/auth/login, and/api/auth/refreshall return503 { error: "backend_unavailable" }instead of mock data, and the hardcoded mock bearer/refresh tokens are rejected. - Switching the in-app Testnet/Mainnet control on
/dashboard/walletsre-fetches/api/wallets?network=<selected>and shows only that network's wallets — with no separate "all networks" filter control left on the page to disagree with it.