Skip to main content

runtime-env-config

Why It Exists

This skill shipped as commit e5cc02d, the first of three commits (runtime-env-configlocal-dev-stackprod-deploy) documented in the docs/sessions/2026-08-02-env-stack-migration dossier as the canonical runtime-env pattern for a ~12-repo personal fleet. It wasn't written speculatively — it was extracted from migrating the first repo, workloom, end-to-end: that migration dropped NEXT_PUBLIC_ usage from 21 occurrences to zero and swapped a repo-local loader for the shared @cogs/config / @cogs/environment packages (both published to npm in the same session). ARCHITECTURE.md names workloom "the concrete instantiation every skill was extracted from," and its diff became the migration template for the remaining 11 repos in docs/ENV-MIGRATION.md.

The underlying decision — one shared loader package in cogs, not a fork per repo — is recorded in docs/adr/0001-canonical-env-schema.md. It reverses an earlier ESM-only mistake in @cogs/config: the package was reworked into a dual ESM+CJS build specifically so a CJS server-preload.cjs could sit next to dd-trace/init and run via NODE_OPTIONS="--require", which the original ESM-only loader couldn't do.

runtime-env-config covers only the app side of that stack — the EnvProvider/setClientConfig/useAccessToken chain inside a single Next.js app. The two sibling skills born in the same session, local-dev-stack and prod-deploy, cover what sits below and downstream of it.

What It Does

runtime-env-config wires environment variables into a Next.js App Router app as runtime config instead of build constants. The server reads process.env on every request (never at build time), hands an explicit snapshot to a client EnvProvider, and the same values drive a Kubb-generated API client's base URLs and per-request auth headers. Change a value and restart the process — there's no rebuild, and the new value flows through on the next request.

Concretely, it scaffolds and documents five pieces: a client EnvProvider (snapshot state, useEnvContext, updateEnv), a module-level runtimeEnvSnapshot mirror for non-React callsites, setClientConfig() to wire *_BASE_URL env vars into the Kubb fetch-client, useAccessToken for request-scoped auth headers, and the root-layout wiring that ties them together behind export const dynamic = 'force-dynamic'.

How To Use It

Triggers on: "runtime env vars", "no NEXT_PUBLIC", "env that changes without rebuild", ".env load order", "env file precedence", "ENV stage selection", "correctEnv/envs", "EnvProvider", "setClientConfig baseURL", "pass headers to react-query hooks", or standing up the env layer of a new app.

skills add git@github.com:catesandrew/next-starters.git --skill skills/runtime-env-config -g
npm install @next-starters/skill-runtime-env-config
/plugin marketplace add catesandrew/next-starters
/plugin install runtime-env-config@next-starters

Gotchas & Invariants

  • No NEXT_PUBLIC_ prefix on anything that varies by environment. NEXT_PUBLIC_ values are inlined at build time, which forces one build per environment and turns a URL/token rotation into a rebuild. Reading process.env at request time is what lets one image promote across dev → qa → prod.
  • The root layout must be dynamic (export const dynamic = 'force-dynamic' in app/layout.tsx) — without it, Next can statically render and freeze process.env reads at build time, defeating the whole pattern.
  • Only Server Components touch process.env directly; a 'use client' file reads env exclusively through useEnvContext(). process.env.FOO inside a client component is a bug, not a shortcut.
  • EnvProvider takes an explicit initialEnv snapshot built one key at a time — never <EnvProvider initialEnv={process.env}> or a spread. Explicit listing is what keeps secrets server-side by omission.
  • updateEnv(key, value) lets a running session swap a value without a restart, and setRuntimeEnvSnapshot/getRuntimeEnvSnapshot give non-React utilities the same read without needing hooks.
  • setClientConfig() runs server-side and must be called from layout.tsx and every page.tsx/route handler that hits the API — it's idempotent, and a page can render without the layout having run in some Next paths.
  • Auth headers come from useAccessToken, threaded explicitly into every generated hook call (client: { headers }) rather than stored as global mutable state — the token stays request-scoped.
  • Direct client→backend calls (as opposed to a same-origin proxy) need an extra client-side setConfigs sync on env change, or updateEnv will change the displayed value without ever repointing a live request.
  • local-dev-stack — the operational stack that sits under this app-side loader: the .env.<stage> file/secret split, process-compose orchestration, and local Supabase port blocks this loader's .env chain actually reads from.
  • prod-deploy — the prod half this loader's env vars must reach, since neither Render nor Vercel ever runs the .env.<stage> file loader and every value must be entered as platform env.

Sourced from: skills/runtime-env-config/metadata.json, skills/runtime-env-config/SKILL.md, docs/sessions/2026-08-02-env-stack-migration/README.md, docs/sessions/2026-08-02-env-stack-migration/ARCHITECTURE.md, docs/sessions/2026-08-02-env-stack-migration/SUMMARY.md, git log --follow -- skills/runtime-env-config (introducing commit e5cc02d)