Reactive Environment Variables with watchEnv
Long-running processes can now react to environment variable changes at runtime — no restart required.
Reactive Environment Variables with watchEnv
Environment variables have always been a "set once at startup" affair — change a value and you restart the process. watchEnv() breaks that pattern for long-running applications.
The Problem
Servers, CLI watchers, and development tooling all suffer from the same friction: you tweak a .env value and have to restart the whole process to pick up the change.
// Traditional: one-shot at process start
const env = defineEnv({ PORT: number().port() })
// PORT is now frozen — any change requires a restart
During development, this means killing and restarting a dev server. In production, it means a rolling deploy or a SIGHUP handler.
Watching for Changes
watchEnv() takes the same schema as defineEnv() but polls the source for changes:
import { watchEnv, string, number } from "@ctroenv/core"
const env = watchEnv(
{ DATABASE_URL: string().url(), PORT: number().port().default(3000) },
{ pollInterval: 2000, onChange: (key, old, next) => console.log(`${key}: ${old} -> ${next}`) },
)
The env object works exactly like a normal defineEnv() result — TypeScript types, secret masking, the env.meta API — but values update automatically when the source changes.
Callbacks
The onChange callback fires with the key, old value, and new value every time a variable changes:
watchEnv(schema, {
onChange(key, oldVal, newVal) {
log.info(`Env var "${key}" changed`, { old: oldVal, new: newVal })
if (key === "LOG_LEVEL") reconfigureLogger(newVal)
},
})
Use this to react to changes — reconfigure loggers, update connection pools, or invalidate caches — without restarting.
Error Handling
If the source changes to values that fail validation, watchEnv doesn't crash:
watchEnv(schema, {
onError(errors) {
console.error("Validation failed on change:", formatErrors(errors))
},
})
The previous valid values remain in place. The env object stays consistent until the source produces valid values again.
Cleanup
Every watchEnv() returns an unwatch() function:
const env = watchEnv(schema)
// Later, when shutting down or no longer needing updates:
env.unwatch()
clearInterval(env.unwatch) // internal timer is cleaned up
This prevents stale watches in long-lived modules, test suites, or hot-reload scenarios.
AbortSignal Support
For frameworks that use AbortController, pass a signal:
const controller = new AbortController()
const env = watchEnv(schema, { signal: controller.signal })
// On shutdown:
controller.abort()
When to Use watchEnv
Good candidates:
- Dev servers — restart-less
.envedits - CLI watchers —
ctroenv validate --watchis built on this - Long-running workers — reconfigure without restart
- Testing — simulate env changes without spawning new processes
Stick with defineEnv() for serverless functions, build-time validation, and one-shot scripts.
Full Changelog
See the GitHub Release for the complete list of changes.