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:
| 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
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
| 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
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.
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>;
}| 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.
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, 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
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 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 undercreateToken('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" }
}
}
}
}| 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.
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:
| 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
omnitron.config.ts— defineEcosystem({...})apps/api/src/bootstrap.ts— defineSystem({...})src/modules/http.module.tssrc/modules/worker.module.tsconfig/default.json— OmnitronAppConfigdist/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 launchInfrastructure —
OmnitronAppConfigdeep diveCLI — 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. 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.