---
id: "omnitron/configuration"
title: "Configuration"
description: "Every field of omnitron.config.ts, defineEcosystem(), defineSystem()."
---

# Configuration

One repository is one [project with many stacks](/docs/omnitron/projects). Use the root
configuration for shared application definitions; additional configurations can
contribute named stacks without creating additional projects.

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

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`

```typescript
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:

```typescript
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](/docs/omnitron/releases).

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.

```typescript
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`

| 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[]`:

```typescript
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;
}
```

| Field            | Purpose                                                           |
| ---------------- | ----------------------------------------------------------------- |
| `name`           | Unique app id — used by every CLI command                         |
| `bootstrap`      | Source or compiled bootstrap path, relative to `cwd` or absolute  |
| `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     |
| `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` |

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](/docs/omnitron/diagnostics#classic-services-and-completed-work).

### `IWatchConfig`

```typescript
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](/docs/omnitron/recipes) 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.

```typescript
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>;
}
```

| Field                     | Effect                                                                                                                                                          |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                    | `local` — single host; `remote` — single remote host; `cluster` — multi-node                                                                                    |
| `infrastructureOwnership` | `stack` (default) owns provisioning/migrations; `controller` is a generated non-owning node allocation; `external` uses explicitly configured existing services |
| `managedOrigin`           | Required source project/stack binding for `controller` allocations; never inferred from a project basename                                                      |
| `watch`                   | Default `true` for managed source apps; disable explicitly when required                                                                                        |
| `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.)                                                                                  |

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](/docs/omnitron/infrastructure#infrastructure-ownership).

### `IStackNode`

```typescript
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`

```typescript
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:

```typescript
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

```typescript
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`

```typescript
monitoring: {
  healthCheck: { interval: 15_000, timeout: 5_000 },
  metrics:     { interval: 5_000, historyMaxAge: '7d' },
}
```

| 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.historyMaxAge` | Persistent history duration (`7d` by default); Titan validates the duration |
| `metrics.retention`     | Legacy numeric setting; does not control the persistent store               |

### `logging`

```typescript
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`

```typescript
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`

```typescript
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`).

```typescript
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

```typescript
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](/docs/omnitron/topology#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

```typescript
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`)

```typescript
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

```typescript
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`):

```json
{
  "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?, quotes? }`                 |
| `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                                                                                                                             |
| `quotes`        | Injects `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](/docs/omnitron/infrastructure).

### Lifecycle hooks

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

```typescript
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](/docs/omnitron/diagnostics#domain-readiness).

### Custom routes

```typescript
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](/docs/omnitron/daemon#daemon-configuration)).
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.

```typescript
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:

```json
{
  "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](/docs/omnitron/backend-handoff) 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](/docs/omnitron/cli#backups).

## Runtime DTOs (returned by RPC)

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

### `ProcessInfoDto`

```typescript
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`

```typescript
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:

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.

## 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. 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

- [Architecture](/docs/omnitron/architecture) — what these configs drive
- [Daemon](/docs/omnitron/daemon) — own config and lifecycle
- [Orchestrator](/docs/omnitron/orchestrator) — what `defineSystem` becomes
  at launch
- [Infrastructure](/docs/omnitron/infrastructure) — `OmnitronAppConfig` deep dive
- [CLI](/docs/omnitron/cli) — commands that read this config

## Interactive SSH policy

`daemon.terminal` accepts `enabled`, `maxSessions`, `maxSessionsPerOwner`, `idleTimeoutMs`, `absoluteTimeoutMs` and `disconnectTimeoutMs`. See the [SSH policy table and example](/docs/omnitron/terminal#session-lifetime-and-limits). 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](/docs/omnitron/files#configure-access-per-node) for configuration examples and boundaries.