Documentation/omnitron/Configuration
In this section

Configuration

One repository is one project with many stacks. Use the root configuration for shared application definitions; additional configurations can contribute named stacks without creating additional projects.

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({ ... })

The ecosystem exports data or a factory as default; the bootstrap exports its system definition. The ecosystem loader checks the selected project directory for omnitron.config.ts, then .js, then .mjs. An explicit file can be selected with loadEcosystemConfigFile. The daemon reads each app's bootstrap 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:     { /* ... */ },
});

Factories and fresh configuration reads

An async factory can separate build inputs from runtime secrets:

import { readFile } from 'node:fs/promises';
import { defineEcosystem } from '@omnitron-dev/omnitron';
import type { EcosystemPhase } from '@omnitron-dev/omnitron/config';
import { selectedApplication } from './selected-registration.js';

export default async ({ phase }: { phase: EcosystemPhase }) => defineEcosystem({
  apps: [{
    name: 'api',
    ...selectedApplication,
    ...(phase === 'runtime' && {
      env: { API_TOKEN: (await readFile(new URL('./.local/api-token', import.meta.url), 'utf8')).trim() },
    }),
  }],
});

runtime is the default phase. The ordinary release builder supplies release; it does not need to read the example's token. The example's selected application must still declare a bootstrap, script or exact release reference. Build inputs and imported registrations participate in source admission.

Every read evaluates the actual file and its full native import graph in a new, short-lived process. Changing a transitive import takes effect on the next reload; there is no retained ESM or CommonJS cache from an earlier selection. TypeScript, import.meta.url, createRequire, the invoking working directory and the current environment preserve their normal meanings. Relative app paths are subsequently anchored to the configuration directory; an explicit absolute cwd is retained.

The factory returns plain declarative objects, arrays and finite primitive values. Optional undefined values are preserved. Functions inside the result, accessors, class instances, cycles, symbols and bigint values are refused rather than silently removed or coerced. Put executable lifecycle hooks in defineSystem, not in the ecosystem result. A factory must not start services or rely on changing the controller's process.env: its process exits after the read. Configuration remains trusted executable code with filesystem and network access; this isolation controls freshness and lifetime, not permissions.

import { loadEcosystemConfig, loadEcosystemConfigFile } from '@omnitron-dev/omnitron/config';

const config = await loadEcosystemConfig(projectDirectory, 'runtime', { timeoutMs: 30_000 });
const build = await loadEcosystemConfigFile(configFile, 'release', { timeoutMs: 30_000 });

EcosystemLoadOptions.timeoutMs defaults to 60 seconds and accepts a positive value up to ten minutes. Definition and ecosystem reads use the same child lifecycle: the caller waits for the result and process exit, a deadline kills the reader, and parent disconnection ends a pending asynchronous read. Output is private and limited to 1 MiB; the returned data is limited to 16 MiB. Failures retain a bounded, credential-redacted reason and cause. Import output does not contaminate CLI JSON.

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
  env?:           Record<string, string>;
  cwd?:           string;      // auto-stamped to project dir
  restartPolicy?: IRestartPolicy;
  startupTimeout?: number;
  watch?:         string | IWatchConfig | false;
  readiness?:     AppReadinessConfig;
}
FieldPurpose
nameUnique app id — used by every CLI command
bootstrapSource or compiled bootstrap path, relative to cwd or absolute
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
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

An ecosystem entry describes one application. Declare replica counts on the individual defineSystem process entries with processes[].instances; a classic script entry starts one process. There is no application-wide instances override. A non-unit multiplier in an existing configuration is rejected before lifecycle changes instead of silently starting fewer processes than requested. Runtime pool resizing uses omnitron scale <app> <count> --process <pool> and does not rewrite the declared topology.

AppReadinessConfig

probes contains 1–16 uniquely named checks. intervalMs (1–300 seconds, default 5 seconds) bounds result reuse; timeoutMs (100 ms–10 seconds, default 3 seconds) bounds each HTTP check. HTTP probes declare url, optional successful expectedStatus, and optional expect with 1–16 exact JSON scalar fields. One heartbeat probe per classic application declares file relative to cwd and maxAgeMs (1 second–24 hours). Combine multiple domain checks in that report. Bootstrap applications report completed work through hooks.onHealthCheck. See readiness and heartbeat examples.

IWatchConfig

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

The filesystem watcher compares whole path segments: ['test'] excludes a test directory; ['test/**'] is not a glob and matches no segment. Defaults include node_modules, dist, .git and test directories. Bootstrap source changes use the compiler's import graph, including workspace libraries. See the development recipe for both watcher modes.

Watch is an application policy within a shared owner. Installed releases are excluded from both watchers and source compilation, even when other applications use development mode. Their releaseRoot marks the installed execution boundary; launch receipts and integrity checks still authenticate its files. Do not add releaseRoot to a source entry to bypass release admission.

watch: false suppresses automatic rebuilding and restarting for a source entry; an explicit restart still compiles its current TypeScript sources. Disabling watch awaits watcher disposal and cancels queued automatic restarts. Enabling it reconciles changes made while it was disabled, without requiring another edit.

If an application is explicitly redefined while a restart is already stopping it, the earlier restart request is cancelled, including an identical redefinition. The application remains stopped with its current definition; inspect it and start it explicitly. Disabling global watch alone lets an already admitted restart finish when the application's definition is unchanged.

Stopping and resuming an installed stack retains its verified release independently of this watch policy. These guarantees require an updated execution owner; updating only the controller cannot change the watcher already loaded in an older owner.

IStackConfig

A stack scopes an environment within a project. Boot and automatic recovery admit enabled stacks independently: local Docker availability does not gate remote attachment or stacks using external infrastructure. Provisioning failures remain attached to the affected stack, which retries with its own exponential backoff from one to fifteen minutes. Running and degraded stacks stay under application supervision; recovery does not restart them wholesale.

interface IStackConfig {
  infrastructureOwnership?: 'stack' | 'controller' | 'external';
  managedOrigin?:      { project: string; stack: string };
  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
infrastructureOwnershipstack (default) owns provisioning/migrations; controller is a generated non-owning node allocation; external uses explicitly configured existing services
managedOriginRequired source project/stack binding for controller allocations; never inferred from a project basename
watchDefault true for managed source apps; disable explicitly when required
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.)

Non-owning modes apply only to local application allocations. They retain explicit application connection settings, reject stack-level infrastructure declarations and serviceOverrides, and ignore ecosystem infrastructure defaults that may belong to other stacks. Requirement discovery does not authorize a node to create replacement services or run their migrations. See infrastructure ownership.

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, historyMaxAge: '7d' },
}
FieldEffect
healthCheck.intervalHow often probes run (default 15 s)
healthCheck.timeoutPer-probe deadline (default 5 s)
metrics.intervalSample cadence (default 5 s)
metrics.historyMaxAgePersistent history duration (7d by default); Titan validates the duration
metrics.retentionLegacy numeric setting; does not control the persistent store

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 proxy in the application's private Netron context. Pools use load-balanced dispatch; singletons follow their current child process.

  • access: [...] — list of Netron service names this process needs. Bootstrap injects proxies into DI under createToken('topology:{ServiceName}').

Service names resolve within one application launch. Identical names in another project or stack are independent; duplicate providers inside one application fail startup. See application service isolation.

access declares injected sibling proxies. Processes in one application share its private service namespace. It does not create OS isolation or per-method authorization between those siblings.

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?, quotes? }
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
quotesInjects quotesRedis — host, port, password and the quotes app's own Redis database index — so this app can read price data at the source instead of asking quotes over RPC

quotes 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?: (app: any) => Promise<{
    status: 'healthy' | 'degraded' | 'unhealthy';
    checks?: Array<{ name: string; status: 'pass' | 'warn' | 'fail'; message?: string }>;
  }>;
}

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. Return named checks with actionable message values: arbitrary fields such as details are not part of the process health contract. The daemon preserves check names and messages for both CLI and Console. See domain readiness.

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). Persist supported instance policy in ~/.omnitron/config.json (or the selected OMNITRON_HOME). CLI flags and environment variables override only the settings they explicitly expose. These are startup settings, not live mutations.

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?: 'postgres', clusterId?, leaseTtlMs?, heartbeatInterval? };
  secrets?:    { provider: 'file'|'env'; path?; passphrase? };
  auth?:       { jwtSecret? };
  healthMonitor?: { intervalMs?, concurrency?, offlineTimeoutMs?, ...retentionDays? };
  applicationReadiness?: Record<string, AppReadinessConfig>; // qualified app names, HTTP fallback
  backups?: { databaseTimeoutMs?: number }; // PostgreSQL dump/restore, default 1800000
}

Database backup policy

Merge this field into the daemon's existing config.json, preserving its other settings and private file permissions:

{
  "backups": {
    "databaseTimeoutMs": 1800000
  }
}

The value is a positive integer in milliseconds, at most 86400000 (24 hours). Omission selects 30 minutes. The same budget covers each local or Docker PostgreSQL dump and restore, including streams, compression and durable file publication. Invalid values refuse startup before backup RPC and schedules become available. S3 object transfers and Tor archives do not use this setting.

A running daemon retains its current policy. Apply a reviewed backend replacement or planned restart to load a change; editing the file alone does not reconfigure it. After activation, omnitron backup schedules --json exposes the effective databaseTimeoutMs, schedules and stack coverage. A larger budget allows a large database more time; it does not reserve disk space or prove the archive can be restored. An expired restore does not undo SQL already applied; see the backup and restore boundaries.

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

Interactive SSH policy

daemon.terminal accepts enabled, maxSessions, maxSessionsPerOwner, idleTimeoutMs, absoluteTimeoutMs and disconnectTimeoutMs. See the SSH policy table and example. This is startup policy enforced by the daemon, independent of browser settings.

File workspace exports

files in ~/.omnitron/config.json configures administrator file access per node; it is not a project ecosystem setting. Omission exposes the / filesystem within daemon OS permissions. Set enabled: false or roots: [] to disable exports. An explicit roots array replaces the default with { id, label, path, readOnly? } entries. See File workspace for configuration examples and boundaries.