runtime-env-config
Why It Exists
This skill shipped as commit e5cc02d, the first of three commits
(runtime-env-config → local-dev-stack → prod-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. Readingprocess.envat request time is what lets one image promote across dev → qa → prod. - The root layout must be dynamic (
export const dynamic = 'force-dynamic'inapp/layout.tsx) — without it, Next can statically render and freezeprocess.envreads at build time, defeating the whole pattern. - Only Server Components touch
process.envdirectly; a'use client'file reads env exclusively throughuseEnvContext().process.env.FOOinside a client component is a bug, not a shortcut. EnvProvidertakes an explicitinitialEnvsnapshot 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, andsetRuntimeEnvSnapshot/getRuntimeEnvSnapshotgive non-React utilities the same read without needing hooks.setClientConfig()runs server-side and must be called fromlayout.tsxand everypage.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
setConfigssync on env change, orupdateEnvwill change the displayed value without ever repointing a live request.
Related Skills
- local-dev-stack — the operational stack that
sits under this app-side loader: the
.env.<stage>file/secret split,process-composeorchestration, and local Supabase port blocks this loader's.envchain 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)