Runtime configuration
NetScript splits configuration into two layers: a typed project config you author
once in netscript.config.ts and load at startup (@netscript/config), and a
hot-reloadable runtime override layer that operators roll out without redeploying
(@netscript/runtime-config). Together they give you one validated, type-safe view of how
the app is wired — defaults baked into the schema, values pulled from a config file and the
environment, and last-mile operational overrides applied at runtime.
What it is
Project config is the static, schema-validated topology of a NetScript app. You author
it with defineConfig (or defineConfigAsync when the shape depends on mode/command),
NetScript validates it against a Zod schema, and loadConfig / initConfig resolve the
authored file into a fully typed NetScriptConfig. Environment variables feed into this
layer through resolveEnv / getEnv, which coerce and default typed values per call.
Runtime config is a separate, dynamic override layer. It lives under a runtime/
directory of versioned JSON files (jobs, sagas, triggers, features, tasks) selected by a
current version pointer. loadRuntimeConfig reads it into a RuntimeConfig snapshot;
watchRuntimeConfig reloads on change. Missing files produce empty defaults so startup
never blocks on overrides that have not been rolled out yet. The two packages are
deliberately distinct: @netscript/config owns the build-time contract, and
@netscript/runtime-config owns the hot-reloadable operational layer.
Learn → / Do →
Minimal example
Author the project config once, then load and inspect it at startup. The runtimeConfig
section of the project config controls where the generated runtime schema/output lives;
the live overrides are read separately by @netscript/runtime-config.
// netscript.config.ts
import { defineConfig } from '@netscript/config';
export default defineConfig({
name: 'orders',
version: '1.0.0',
databases: {
active: 'postgres',
config: [{ provider: 'postgres', schema: 'database/postgres/schema' }],
},
services: {
api: { port: 3000 },
},
plugins: ['@netscript/plugin-workers'],
});
// src/bootstrap.ts — load project config + apply runtime overrides
import { initConfig, getConfig } from '@netscript/config';
import { isFeatureEnabled, loadRuntimeConfig } from '@netscript/runtime-config';
// 1. Project config: validated once, then read synchronously anywhere via getConfig().
await initConfig();
const config = getConfig();
// 2. Runtime overrides: hot-reloadable JSON the operator controls (empty if unset).
const runtime = await loadRuntimeConfig();
const rolloutOn = isFeatureEnabled(runtime, 'worker-rollout', false);
console.log(config.name, config.services?.api?.port, rolloutOn);
Key types first
The project config you author is NetScriptConfigInput; what loadConfig / initConfig
return is the fully validated NetScriptConfig. These are the load-bearing fields.
| Name | Type | Description |
|---|---|---|
name |
string (required) |
Project name. |
version |
string (required) |
Project version. |
paths |
PathsConfig (required) |
Workspace path conventions (services, packages, apps, workers, sagas, triggers, plugins, contracts, database, tasks, deploy). |
databases |
DatabasesConfig (required) |
{ active?: provider, config: DatabaseConfig[] } — configured databases and the active selector. |
plugins |
string[] (required) |
Enabled plugin package names or specifiers. |
services |
Record |
Service configuration by service name. |
apps |
Record |
Frontend application configuration by app name. |
logging |
LoggingConfig? |
{ level, format, timestamps, colors? } logging behavior. |
aspire |
AspireConfig? |
{ appHost, dashboardPort } Aspire orchestration settings. |
sagas / triggers / gateway / sdk / deploy |
section objects? |
Optional per-capability config sections. |
runtimeConfig |
RuntimeConfigSection? |
Runtime schema/config OUTPUT settings (where generated runtime schema is written) — not the live overrides. |
The runtime override snapshot is a separate type — RuntimeConfig — returned by
loadRuntimeConfig(). Each field is an array loaded from one topic directory.
| Name | Type | Description |
|---|---|---|
jobs |
JobOverride[] |
Job overrides from runtime/jobs/*.json (enabled, schedule, timeout, maxRetries, timezone, concurrency). |
sagas |
SagaOverride[] |
Saga overrides from runtime/sagas/*.json (enabled, timeout, maxRetries, compensationTimeout). |
triggers |
TriggerOverride[] |
Trigger overrides from runtime/triggers/*.json (enabled, paths). |
features |
FeatureFlag[] |
Feature flags from runtime/features/*.json (id, enabled, description?, rolloutPercentage?). |
tasks |
RuntimeTask[] |
Runtime task definitions from runtime/tasks/*.json (id, name, runtime, entrypoint, enabled?, timeout?, schedule?). |
Resolution & precedence
There is no single monolithic merge function — resolution happens across the two packages, each with its own precedence. The order below is the conceptual chain the diagram depicts.
| Name | Type | Description |
|---|---|---|
1. Schema defaults |
@netscript/config |
Zod schema fills omitted fields when defineConfig validates the authored input into NetScriptConfig. |
2. Config file |
loadConfig / initConfig |
File search order: options.configFile → netscript.config.ts → .js → .mjs. cwd resolves from options.cwd → NETSCRIPT_PROJECT_ROOT → Deno.cwd(). |
3. Environment |
resolveEnv / getEnv |
Per-variable: read the env var (def.env overrides the name) and coerce by type (string|number|boolean|json); if absent, use default; if required and absent with no default, throw. |
4. Runtime overrides |
loadRuntimeConfig |
Hot-reloadable JSON under the runtime dir, keyed by the 'current' version pointer; applied per-id (job/saga/trigger/feature/task). Missing files → empty defaults. |
| Name | Type | Description |
|---|---|---|
defineConfig(input) |
(NetScriptConfigInput) => NetScriptConfig |
Type-safe, validated config definition for netscript.config.ts. The 80% path. |
defineConfigAsync(fn) |
((env: ConfigEnv) => …) => () => Promise |
Async/env-aware config when the shape depends on { mode, command }. |
loadConfig(options?) |
(LoadConfigOptions?) => Promise |
Find, import, and validate the config file. Options: { cwd?, configFile? }. |
initConfig(options?) |
(LoadConfigOptions?) => Promise |
Load once and cache; call at startup. Then read synchronously via getConfig(). |
getConfig() |
() => NetScriptConfig |
Synchronous access to the cached config; throws if initConfig() has not run. |
isConfigLoaded() / clearConfigCache() |
() => boolean / () => void |
Inspect or reset the config cache (clearConfigCache is for tests/reload). |
resolveEnv(schema) / getEnv(name, opts?) |
typed env readers |
Coerce env vars (string|number|boolean|json) with defaults; getEnv reads one variable. |
getMode() / isDev() / isProd() / isTest() / hasEnv(name) |
env helpers |
Current mode (DENO_ENV/NODE_ENV) and presence checks. |
discoverWorkspace(root?) / findWorkspaceRoot(dir) / findMember(ws, name) |
workspace |
Classify and locate Deno workspace members. |
inspectConfig(target) |
(Partial |
JSON-stable diagnostic report for CLI rendering. |
| Name | Type | Description |
|---|---|---|
loadRuntimeConfig() |
() => Promise |
Load overrides from the runtime dir via the 'current' pointer; empty defaults when files are missing. |
watchRuntimeConfig(onChange, opts?) |
(cb, { signal?, prefix? }) => void |
Watch the runtime config files and invoke onChange after debounced reloads. |
isFeatureEnabled(config, flagId, defaultValue) |
(RuntimeConfig, string, boolean) => boolean |
Read a feature flag, falling back when the flag is absent. |
getJobOverride / getSagaOverride / getTriggerOverride |
(RuntimeConfig, id) => …Override | undefined |
Look up a single override by id. |
getRuntimeTask(config, taskId) |
(RuntimeConfig, string) => RuntimeTask | undefined |
Get a runtime task definition by id. |
summarizeRuntimeConfig(config, prefix?) |
(RuntimeConfig, string?) => RuntimeConfigSummary |
Structured summary of active overrides (disabled jobs/sagas/triggers/features, message lines) for caller-owned presentation. |
RUNTIME_CONFIG_TOPICS |
('jobs'|'sagas'|'triggers'|'features'|'tasks')[] |
The topic names backed by versioned JSON files. |
Runtime override layout
The runtime override directory is resolved from the environment, then read through the
current pointer that names the active version of each topic file.
// src/feature-gate.ts
import { isFeatureEnabled, loadRuntimeConfig } from '@netscript/runtime-config';
const runtime = await loadRuntimeConfig();
// Third arg is the fallback when the flag file/entry is absent.
if (isFeatureEnabled(runtime, 'worker-rollout', false)) {
// run the gated path
}
// src/runtime-watch.ts
import { summarizeRuntimeConfig, watchRuntimeConfig } from '@netscript/runtime-config';
const controller = new AbortController();
watchRuntimeConfig(async (config) => {
const summary = summarizeRuntimeConfig(config);
// re-apply overrides; summary.messages is presentation-ready
console.log(summary.messages.join('\n'));
}, { signal: controller.signal });
// src/env.ts
import { getEnv, resolveEnv } from '@netscript/config';
const port = getEnv('PORT', { type: 'number', default: 3000 });
const env = resolveEnv({
DATABASE_URL: { env: 'DB_URL', required: true },
DEBUG: { type: 'boolean', default: false },
});
Production notes
Reference →
This hub is intentionally thin — the full generated API surface for both packages lives in the reference.