Skip to main content

Configuration

Omnitron is driven by two files:

FilePurposeImports
omnitron.config.ts (repo root)Ecosystem — what apps exist, stacks, infrastructuredefineEcosystem({ ... })
apps/<name>/src/bootstrap.ts (per-app)System — what processes the app runsdefineSystem({ ... })

Both files export their result as default. The CLI auto-locates the ecosystem config from CWD upward; the daemon hands each app's bootstrap to the orchestrator at launch.

Verified against apps/omnitron/src/config/ (types.ts, define-ecosystem.ts, define-system.ts, defaults.ts).

defineEcosystem(config)omnitron.config.ts

import { defineEcosystem } from '@omnitron-dev/omnitron';

export default defineEcosystem({
project: 'my-monorepo',
apps: [/* IEcosystemAppEntry[] */],
stacks: { /* StackName → IStackConfig */ },
infrastructure: { /* ... */ },
gateway: { /* ... */ },
supervision: { /* ... */ },
monitoring: { /* ... */ },
logging: { /* ... */ },
});

Top-level fields — IEcosystemConfig

FieldTypeRequiredDefault
projectstring
appsIEcosystemAppEntry[]
stacksRecord<StackName, IStackConfig>
infrastructureInfrastructureConfig
gateway{ port?, configDir?, image? }port 8080, image openresty/openresty:alpine
supervision{ strategy, maxRestarts, window, backoff }one_for_one / 5 / 60 000 ms / expo
monitoring{ healthCheck, metrics }15 s / 5 s
logging{ level, maxSize, maxFiles, compress, databaseRetentionDays }info / 50 MB / 10 / true / 14 days

StackName is 'dev' | 'test' | 'staging' | 'prod' | string.

IEcosystemAppEntry

Each entry in apps[]:

interface IEcosystemAppEntry {
name: string;
bootstrap?: string; // bootstrap mode (preferred)
script?: string; // classic mode (legacy)
enabled?: boolean; // default true
critical?: boolean; // daemon shuts down if this crashes past maxRestarts
dependsOn?: string[]; // start order
instances?: number;
env?: Record<string, string>;
cwd?: string; // auto-stamped to project dir
restartPolicy?: IRestartPolicy;
startupTimeout?: number;
watch?: string | IWatchConfig | false;
}
FieldPurpose
nameUnique app id — used by every CLI command
bootstrapAbsolute path to compiled bootstrap.js (bootstrap mode)
scriptPath to a classic main.ts (legacy single-process apps)
enabledfalse skips during startAll
criticalCrashing past maxRestarts brings the daemon down
dependsOnWait until these apps report healthy before starting this one
instancesOverride defineSystem process count (overrides app-internal value)
envApp-level env merged over process env
cwdBase for relative paths; auto-stamped to the config's directory
restartPolicyPer-app override of ecosystem supervision
startupTimeoutMax ms for ready handshake before treated as failed
watchDev-mode file watching: string=directory; full config; or false

IWatchConfig

interface IWatchConfig {
directory: string; // relative to cwd or absolute
include?: string[]; // additional directories
ignore?: string[]; // glob patterns; merged with defaults
debounce?: number; // default 300 ms
}

Default ignored patterns include node_modules, dist, .git, log files, and source maps. Add to ignore rather than replace.

IStackConfig

A stack scopes an environment within a project:

interface IStackConfig {
type: 'local' | 'remote' | 'cluster';
watch?: boolean;
nodes?: IStackNode[];
infrastructure?: Partial<InfrastructureConfig>;
settings?: IStackSettings;
apps?: string[] | 'all';
portRange?: { start: number; end: number };
serviceOverrides?: Record<string, IServiceOverride>;
}
FieldEffect
typelocal — single host; remote — single remote host; cluster — multi-node
watchDefault true for dev, false otherwise
nodesFor remote/cluster: list of IStackNode participants
infrastructurePer-stack override of ecosystem infrastructure
settingsContainer prefix, port offsets, env, log level
apps'all' or explicit list — which ecosystem apps run in this stack
portRangeAuto-allocate ports for stack services within this window
serviceOverridesPer-service override of declared infrastructure (point at external host, etc.)

IStackNode

interface IStackNode {
host: string;
port?: number; // daemon port; default 9700
role: 'app' | 'database' | 'cache' | 'gateway' | 'worker' | 'master';
apps?: string[]; // default 'all'
ssh?: { user?; privateKey?; port? };
label?: string;
}

Each remote node runs its own omnitron daemon (typically as a slave). The master daemon coordinates state replication via the OmnitronSync service.

IStackSettings

interface IStackSettings {
redisDbOffset?: number; // stack gets non-overlapping Redis DB range
containerPrefix?: string; // default `${project}-${stack}`
env?: Record<string, string>;
logLevel?: 'fatal'|'error'|'warn'|'info'|'debug'|'trace';
portOffsets?: { postgres?, redis?, minio? };
}

Port offsets let you run dev and staging stacks side-by-side on the same host:

stacks: {
dev: { type: 'local', settings: { redisDbOffset: 0, portOffsets: { postgres: 0, redis: 0, minio: 0 } } },
staging: { type: 'local', settings: { redisDbOffset: 5, portOffsets: { postgres: 10, redis: 10, minio: 10 } } },
}

supervision — restart policy

supervision: {
strategy: 'one_for_one' | 'one_for_all' | 'rest_for_one';
maxRestarts: number;
window: number; // ms
backoff: IBackoffOptions; // from titan-pm
}
StrategyBehaviour
one_for_oneRestart only the crashed app; others continue
one_for_allRestart all apps when one crashes (atomic stack restart)
rest_for_oneRestart the crashed app + every app started after it (cascade fix)

Defaults: one_for_one / 5 restarts / 60 000 ms window / exponential backoff { initial: 1 000, max: 30 000, factor: 2 }.

monitoring

monitoring: {
healthCheck: { interval: 15_000, timeout: 5_000 },
metrics: { interval: 5_000, retention: 3_600 },
}
FieldEffect
healthCheck.intervalHow often probes run (default 15 s)
healthCheck.timeoutPer-probe deadline (default 5 s)
metrics.intervalSample cadence (default 5 s)
metrics.retentionIn-memory ring buffer depth in seconds

logging

logging: {
level: 'info', // fatal | error | warn | info | debug | trace
maxSize: '50mb', // per-app file size threshold
maxFiles: 10, // rotated files to keep
compress: true, // gzip rotated files
databaseRetentionDays: 14, // age limit for rows in the `logs` table
}

maxSize and maxFiles bound the rotated files on disk. databaseRetentionDays bounds the table — a different store with a different failure mode, and until it existed the table had no bound at all: the development host reached 13 GB of logs. Two weeks is long enough to investigate an incident from the weekend and short enough that the table stops being the largest thing on the disk.

Retention deletes rows; it does not shrink the file. Postgres keeps the freed pages inside the table and reuses them, so a table that has been large stays large on disk — doctor reports that separately as db.bloated and tells you to reclaim it with VACUUM FULL or pg_repack. A logs table far bigger than its live row count means retention is working and the space is simply not being returned.

The daemon turns retention off when the key is absent from the config it receives — a wrong default chosen inside the collector would delete history nobody asked it to delete. Configs loaded the normal way carry the default above; one assembled by hand does not.

Per-app overrides live in IAppDefinition.observability.logging (see defineSystem below).

gateway

gateway: {
port: 8080,
configDir: 'infra/nginx',
image: 'openresty/openresty:alpine',
}

When configured, Omnitron provisions an OpenResty/Lua gateway container per stack — reverse proxy, maintenance mode (driven from Redis), rate limiting, future PoW captcha.

defineSystem(definition) — per-app bootstrap.ts

import { defineSystem } from '@omnitron-dev/omnitron';

export default defineSystem({
name: 'api',
version: '1.0.0',
processes: [
{
name: 'http',
module: './modules/http.module.js',
transports: { http: { port: 3000 } },
health: { enabled: true },
},
{
name: 'worker',
module: './modules/worker.module.js',
instances: 3,
scaling: { strategy: 'auto', maxInstances: 10, targetCPU: 70 },
},
],
observability: { metrics: true, logging: { level: 'info' } },
});

Top-level fields — IAppDefinition

FieldTypeRequired
namestring
versionstring
processesIProcessEntry[] — must contain ≥ 1
requiresIAppRequirements (deprecated; use omnitronConfig.infrastructure in config/default.json)
authApp-level auth manager / JWT config
config{ sources?, envPrefix? }
shutdown{ timeout?, priority?, drainConnections? }
hooksbeforeCreate, afterCreate, beforeStart, afterStart, beforeStop, afterStop, onHealthCheck
omnitronConfigOmnitronAppConfig — declarative infra (database, redis, s3, custom services)
envRecord<string, string>
cwdstring
observability{ metrics?, tracing?, logging? }

IProcessEntry — the heart of defineSystem

Each entry becomes exactly one fork (or a pool of N forks when instances > 1).

interface IProcessEntry {
name: string;
module: string; // path to the @Module file
critical?: boolean;
instances?: number; // > 1 = PM pool with load balancing
topology?: { expose?: boolean; access?: string[] };
transports?: { http?: IHttpTransportConfig; websocket?: IWebSocketTransportConfig };
auth?: IAppDefinition['auth'] | false;
health?: { enabled?, interval?, timeout?, retries? };
startupTimeout?: number; // default 30_000
scaling?: { strategy?, maxInstances?, targetCPU?, targetMemory?, queueThreshold?, cooldownPeriod? };
restartPolicy?: IRestartPolicy;
env?: Record<string, string>;
customRoutes?: Array<{ method, pattern, handler }>;
hooks?: /* same shape as IAppDefinition.hooks */;
observability?: { metrics?, logging? };
}

Topology — cross-process service mesh

processes: [
// Worker exposes its @Service to other processes:
{ name: 'aggregator', module: './aggregator.module.js',
topology: { expose: true } },

// Consumer injects proxies to specific sibling services:
{ name: 'gateway', module: './gateway.module.js',
topology: { access: ['AggregatorService'] } },
]
  • expose: true — Omnitron auto-discovers @Service metadata from this process and registers a load-balanced proxy on daemon Netron (pool processes only).
  • access: [...] — list of Netron service names this process needs. Bootstrap injects proxies into DI under createToken('topology:{ServiceName}').

access is a DI convenience, not a security boundary — all processes managed by omnitron share the daemon's Unix socket.

Transports

interface IHttpTransportConfig {
port: number;
host?: string;
cors?: any;
// ...
}

interface IWebSocketTransportConfig {
port: number;
host?: string;
path?: string;
// ...
}

Set transports.http.port or .websocket.port to expose Netron on those protocols from this specific process.

Scaling (pool mode, instances > 1)

scaling: {
strategy: 'auto' | 'fixed',
maxInstances: 10,
targetCPU: 70, // % per instance
targetMemory: 80, // % heap
queueThreshold: 100, // pending RPC calls before scale-up
cooldownPeriod: 60_000,
}

auto strategy uses titan-pm autoscaler — the orchestrator adds / removes workers based on the targets. fixed keeps the declared instances count regardless of load.

Per-process auth

processes: [
// Inherit app-level auth:
{ name: 'http', module: './http.js' },

// Override:
{ name: 'metrics', module: './metrics.js', auth: false }, // explicitly no auth
{ name: 'admin', module: './admin.js',
auth: { jwt: { enabled: true, tokenCacheTtl: 60 } } },
]

auth: false explicitly disables; omitted inherits the app's auth.

OmnitronAppConfig — declarative infrastructure (per-app config/default.json)

Lives in config/default.json under the omnitron key (not in bootstrap.ts):

{
"omnitron": {
"database": true,
"redis": true,
"s3": { "bucket": "uploads", "quota": "10gb" },
"services": {
"discovery": true,
"notifications": true
},
"infrastructure": {
"search": {
"type": "daemon",
"ports": { "http": 9200 },
"env": { "SEARCH_URL": "http://${host}:${port:http}" },
"docker": { "image": "elasticsearch:8.11.0" }
}
}
}
}
FieldType
databaseboolean | { dialect?, pool?, extensions?, dedicated? }
redisboolean | { prefix?, dedicated? }
s3boolean | { bucket?, quota? }
services{ discovery?, notifications?, priceverse? }
infrastructureRecord<string, IServiceRequirement> — custom containers

services switches on shared capabilities the daemon wires for you rather than infrastructure it provisions:

FlagWhat it wires
discoveryRegisters the app with service discovery
notificationsConnects the app to the notifications Redis database
priceverseInjects priceverseRedis — host, port, password and the priceverse app's own Redis database index — so this app can read price data at the source instead of asking priceverse over RPC

priceverse is the one worth knowing exists: it is cross-app by design, the database index is allocated by the daemon rather than written by you, and nothing else gives an app a connection into another app's Redis.

Omnitron reads these, provisions missing infrastructure, and injects resolved env vars (DATABASE_URL, REDIS_URL, SEARCH_URL) into the app at startup. The app never hardcodes infrastructure addresses.

→ Full reference: Infrastructure.

Lifecycle hooks

App- and process-level hooks carry the same shape:

hooks: {
beforeCreate?: () => Promise<void>;
afterCreate?: (app: any) => Promise<void>;
beforeStart?: (app: any) => Promise<void>;
afterStart?: (app: any) => Promise<void>;
beforeStop?: (app: any) => Promise<void>;
afterStop?: () => Promise<void>;
onHealthCheck?: () => Promise<{ status: 'healthy' | 'degraded' | 'unhealthy' }>;
}

Process-level hooks override app-level. Use beforeStart for schema sanity checks, afterStart for cache priming, onHealthCheck to surface custom checks to the health probe.

Custom routes

processes: [{
name: 'http',
module: './http.module.js',
transports: { http: { port: 3000 } },
customRoutes: [
{ method: 'GET', pattern: '/healthz',
handler: async () => new Response('ok', { status: 200 }) },
{ method: 'POST', pattern: '/webhooks/github',
handler: async (req) => handleGithub(req) },
],
}]

Custom routes sit alongside the Netron-exposed services on the same HTTP transport.

Daemon config — IDaemonConfig (not project-config)

The daemon's own config — paths, ports, role, cluster, secrets, health-monitor — is not in omnitron.config.ts. Defaults are baked in (see Daemon / config defaults). Override via CLI flags on omnitron up or environment variables.

interface IDaemonConfig {
socketPath: string; // ~/.omnitron/daemon.sock
port: number; // TCP, default 9700
host: string; // bind address; default 127.0.0.1
httpPort: number; // default 9800
pidFile: string; // ~/.omnitron/daemon.pid
stateFile: string; // ~/.omnitron/state.json
logDir: string; // ~/.omnitron/logs
role: 'master' | 'slave';
master?: { host: string; port: number };
sync?: ISyncConfig;
cluster?: { enabled, discovery, peers?, electionTimeout?, heartbeatInterval? };
secrets?: { provider: 'file'|'env'; path?; passphrase? };
auth?: { jwtSecret? };
healthMonitor?: { intervalMs?, concurrency?, offlineTimeoutMs?, ...retentionDays? };
}

Runtime DTOs (returned by RPC)

These shapes appear in omnitron status JSON output and in webapp data.

ProcessInfoDto

interface ProcessInfoDto {
name: string;
pid: number | null;
status: 'stopped' | 'starting' | 'online' | 'stopping' | 'errored' | 'crashed';
cpu: number;
memory: number;
uptime: number;
restarts: number;
instances: number;
port: number | null;
mode: 'classic' | 'bootstrap';
critical: boolean;
processes?: SubProcessInfoDto[]; // bootstrap mode
}

DaemonStatusDto

interface DaemonStatusDto {
version: string;
pid: number;
uptime: number;
apps: ProcessInfoDto[];
totalCpu: number;
totalMemory: number;
}

AggregatedMetricsDto, AggregatedHealthDto, LogEntryDto, AppDiagnosticsDto

Defined in src/config/types.ts — refer there for exact field shapes. The CLI and webapp consume them directly through the typed DaemonClient.

Validation

defineSystem() validates synchronously at module load:

CheckThrows
name missing or non-stringDefineSystemError
processes emptyDefineSystemError('At least one process must be defined')
Process without nameDefineSystemError('Every process must have a name')
Process without moduleDefineSystemError('Process X must have a module path')
Duplicate process nameDefineSystemError('Duplicate process name X')
HTTP port not integer 1–65535DefineSystemError('Process X has invalid HTTP port N')
WebSocket port not integer 1–65535DefineSystemError('Process X has invalid WebSocket port N')

Invalid configs fail at import('bootstrap.js') — the daemon surfaces the error and marks the app errored.

Loader behaviour

loader.ts auto-detects omnitron.config.ts from CWD upward when the CLI runs. Resolution order:

  1. Explicit --config <path> on omnitron up.
  2. omnitron.config.ts in CWD.
  3. omnitron.config.ts walking parents up to the filesystem root.

cwd on IEcosystemAppEntry is auto-stamped to the directory of the config that declared the app — relative paths inside the app entry resolve against the project root, not the daemon's working directory. Manual override is rarely needed.

my-monorepo/
├── omnitron.config.ts # defineEcosystem({...})
├── apps/
│ ├── api/
│ │ ├── src/bootstrap.ts # defineSystem({...})
│ │ ├── src/modules/http.module.ts
│ │ ├── src/modules/worker.module.ts
│ │ ├── config/default.json # OmnitronAppConfig
│ │ └── dist/bootstrap.js # compiled artefact
│ └── ingest/
│ └── ...
├── infra/
│ └── nginx/ # gateway configs (referenced by ecosystem.gateway)
└── package.json

Anti-patterns

  • Putting secrets in omnitron.config.ts. The file is committed. Use omnitron secret set and read via process.env inside the app.
  • Mixing bootstrap and script on the same app. Pick one: bootstrap for new code, script only for legacy classic apps.
  • Hardcoding port numbers across stacks. Use portOffsets so dev / staging can coexist on one host.
  • instances > 1 without topology.expose. Pool workers are invisible to siblings without expose. Either add it or drop the pool.
  • critical: true everywhere. Then any crash brings the daemon down. Reserve critical for the one app whose absence truly invalidates the deployment.
  • Hand-editing config/default.json to add infrastructure instead of using OmnitronAppConfig. The schema is validated — declarative is the right path.

See also