Configuration
Omnitron is driven by two files:
| File | Purpose | Imports |
|---|---|---|
omnitron.config.ts (repo root) | Ecosystem — what apps exist, stacks, infrastructure | defineEcosystem({ ... }) |
apps/<name>/src/bootstrap.ts (per-app) | System — what processes the app runs | defineSystem({ ... }) |
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
| Field | Type | Required | Default |
|---|---|---|---|
project | string | — | |
apps | IEcosystemAppEntry[] | ✓ | — |
stacks | Record<StackName, IStackConfig> | — | |
infrastructure | InfrastructureConfig | — | |
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;
}
| Field | Purpose |
|---|---|
name | Unique app id — used by every CLI command |
bootstrap | Absolute path to compiled bootstrap.js (bootstrap mode) |
script | Path to a classic main.ts (legacy single-process apps) |
enabled | false skips during startAll |
critical | Crashing past maxRestarts brings the daemon down |
dependsOn | Wait until these apps report healthy before starting this one |
instances | Override defineSystem process count (overrides app-internal value) |
env | App-level env merged over process env |
cwd | Base for relative paths; auto-stamped to the config's directory |
restartPolicy | Per-app override of ecosystem supervision |
startupTimeout | Max ms for ready handshake before treated as failed |
watch | Dev-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>;
}
| Field | Effect |
|---|---|
type | local — single host; remote — single remote host; cluster — multi-node |
watch | Default true for dev, false otherwise |
nodes | For remote/cluster: list of IStackNode participants |
infrastructure | Per-stack override of ecosystem infrastructure |
settings | Container prefix, port offsets, env, log level |
apps | 'all' or explicit list — which ecosystem apps run in this stack |
portRange | Auto-allocate ports for stack services within this window |
serviceOverrides | Per-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
}
| Strategy | Behaviour |
|---|---|
one_for_one | Restart only the crashed app; others continue |
one_for_all | Restart all apps when one crashes (atomic stack restart) |
rest_for_one | Restart 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 },
}
| Field | Effect |
|---|---|
healthCheck.interval | How often probes run (default 15 s) |
healthCheck.timeout | Per-probe deadline (default 5 s) |
metrics.interval | Sample cadence (default 5 s) |
metrics.retention | In-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
| Field | Type | Required |
|---|---|---|
name | string | ✓ |
version | string | ✓ |
processes | IProcessEntry[] — must contain ≥ 1 | ✓ |
requires | IAppRequirements (deprecated; use omnitronConfig.infrastructure in config/default.json) | |
auth | App-level auth manager / JWT config | |
config | { sources?, envPrefix? } | |
shutdown | { timeout?, priority?, drainConnections? } | |
hooks | beforeCreate, afterCreate, beforeStart, afterStart, beforeStop, afterStop, onHealthCheck | |
omnitronConfig | OmnitronAppConfig — declarative infra (database, redis, s3, custom services) | |
env | Record<string, string> | |
cwd | string | |
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@Servicemetadata 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 undercreateToken('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" }
}
}
}
}
| Field | Type |
|---|---|
database | boolean | { dialect?, pool?, extensions?, dedicated? } |
redis | boolean | { prefix?, dedicated? } |
s3 | boolean | { bucket?, quota? } |
services | { discovery?, notifications?, priceverse? } |
infrastructure | Record<string, IServiceRequirement> — custom containers |
services switches on shared capabilities the daemon wires for you rather
than infrastructure it provisions:
| Flag | What it wires |
|---|---|
discovery | Registers the app with service discovery |
notifications | Connects the app to the notifications Redis database |
priceverse | Injects 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:
| Check | Throws |
|---|---|
name missing or non-string | DefineSystemError |
processes empty | DefineSystemError('At least one process must be defined') |
Process without name | DefineSystemError('Every process must have a name') |
Process without module | DefineSystemError('Process X must have a module path') |
| Duplicate process name | DefineSystemError('Duplicate process name X') |
| HTTP port not integer 1–65535 | DefineSystemError('Process X has invalid HTTP port N') |
| WebSocket port not integer 1–65535 | DefineSystemError('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:
- Explicit
--config <path>onomnitron up. omnitron.config.tsin CWD.omnitron.config.tswalking 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.
Recommended layout
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. Useomnitron secret setand read viaprocess.envinside the app. - Mixing
bootstrapandscripton the same app. Pick one:bootstrapfor new code,scriptonly for legacy classic apps. - Hardcoding port numbers across stacks. Use
portOffsetsso dev / staging can coexist on one host. instances > 1withouttopology.expose. Pool workers are invisible to siblings withoutexpose. Either add it or drop the pool.critical: trueeverywhere. Then any crash brings the daemon down. Reservecriticalfor the one app whose absence truly invalidates the deployment.- Hand-editing
config/default.jsonto add infrastructure instead of usingOmnitronAppConfig. The schema is validated — declarative is the right path.
See also
- Architecture — what these configs drive
- Daemon — own config and lifecycle
- Orchestrator — what
defineSystembecomes at launch - Infrastructure —
OmnitronAppConfigdeep dive - CLI — commands that read this config