Skip to main content
Alpha

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.

Configuration resolution chain: schema defaults flow into the loaded config file, environment variables override typed values via resolveEnv, and hot-reloadable runtime override files apply last to produce a frozen resolved config.
Resolution chain — schema defaults → config file (defineConfig / loadConfig) → environment (resolveEnv / getEnv) → runtime override files (loadRuntimeConfig) → the validated config the framework reads.

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.

NetScriptConfig — the validated project config (returned by loadConfig / initConfig / getConfig)
NameTypeDescription
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.

RuntimeConfig — the hot-reloadable override snapshot (returned by loadRuntimeConfig)
NameTypeDescription
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.

How a value is resolved (lowest precedence first)
NameTypeDescription
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.
@netscript/config — primary functions
NameTypeDescription
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 | string) => InspectionReport JSON-stable diagnostic report for CLI rendering.
@netscript/runtime-config — primary functions
NameTypeDescription
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.

runtime-config