Skip to content

Configuration reference

See also: Environment variable reference — shared env vars; orchestrator-specific vars are listed below. Regenerate the generated table with pnpm docs:env. Unknown KICI_* env vars cause the orchestrator to refuse to start (typo catcher); set KICI_DEV=true for warn-only behaviour during local development.

The KiCI orchestrator uses a layered configuration system: a local YAML file for per-instance settings, a shared PostgreSQL-backed config store for shared settings, and environment variable overrides for both. The scaler configuration (scalers.yaml) remains a separate file.

The orchestrator resolves its configuration from four sources, in order of precedence:

  1. Environment variables (KICI_-prefixed) — highest precedence
  2. Local YAML file (orchestrator.yaml) — per-instance settings
  3. Shared DB config (PostgreSQL config_versions table) — shared across all instances
  4. Built-in defaults — lowest precedence

This means an environment variable always overrides the same setting from YAML or DB. The YAML file overrides the shared DB config. And the shared DB config overrides built-in defaults.

The configuration source of truth is packages/orchestrator/src/config/schema.ts.

The orchestrator loads configuration in two phases to avoid a circular dependency (you need the database URL to connect to the DB, but the DB stores shared config):

  1. Phase 1 (local-only): Load orchestrator.yaml + KICI_ env vars to get database.url, instance.id, server.port, and instance.mode. This is enough to start the process and connect to PostgreSQL.
  2. Phase 2 (full merge): Query the shared config from the config_versions table, merge all four layers (env > YAML > DB > defaults), and validate with the full appConfigSchema.

The local config file contains per-orchestrator-instance settings that are never shared across orchestrators. By default, the orchestrator looks for the file at /etc/kici/orchestrator.yaml. Override this with:

  • --config /path/to/orchestrator.yaml (CLI flag)
  • KICI_CONFIG=/path/to/orchestrator.yaml (env var)

If no file is found and no explicit path was given, the orchestrator runs in env-only mode (YAML is optional).

/etc/kici/orchestrator.yaml
# Local configuration for a single orchestrator instance.
# Database connection (required)
database:
url: 'postgresql://kici:s3cur3pass@postgres:5432/kici'
# Instance settings
instance:
# Unique identifier for this orchestrator instance.
# Default: auto-generated random UUID.
id: 'orch-west-1'
# Operating mode: platform | hybrid | observed | independent
# - platform (default): WS to Platform relay only; rejects direct webhooks.
# Requires platform.url + platform.token (or KICI_PLATFORM_URL + KICI_PLATFORM_TOKEN).
# - hybrid: Platform relay + direct per-source webhook ingestion (deduplicated).
# Requires Platform credentials. Per-source webhook secrets live in the
# orchestrator DB (kici-admin source add ...) — there is no global
# webhook-secret env var.
# - observed: direct per-source webhook ingestion only, but keeps the Platform
# connection for the hosted dashboard. No webhook ever transits KiCI.
# Requires Platform credentials AND KICI_WEBHOOK_PUBLIC_URL. GitHub-App
# sources are refused (they are relay-only) — use generic/local sources.
# - independent: standalone, direct per-source webhook ingestion only.
# Different entry point (`standalone.js`). Per-source secrets in DB.
# Mode is optional — defaults to "platform" — but the credentials the mode
# requires must be present at startup or the orchestrator refuses to boot.
# See operator/orchestrator/getting-started.md#four-deployment-modes for the
# full picture.
mode: 'hybrid'
# HTTP server settings
server:
# Port to listen on (default: 4000)
port: 4000
# URL prefix for all routes (default: "/")
basePath: '/'
# Log level: debug | info | warn | error (default: info)
logLevel: 'info'
# Path to TLS certificate (PEM) for the expiry diagnostic check.
# Optional — when set, /diagnostics reports cert validity and expiry.
# tlsCertPath: '/etc/ssl/certs/kici.pem'
# Auto-scaler configuration file paths
scaler:
# Path to the main scalers.yaml config file
configPath: '/etc/kici/scalers.yaml'
# Directory for scalers.d/ drop-in configs
configDir: '/etc/kici/scalers.d/'
FieldTypeDefaultDescription
database.urlstring(required)PostgreSQL connection URL
instance.idstring<random-UUID>Unique orchestrator instance ID
instance.modeenumplatformOperating mode: platform, hybrid, observed, independent
server.portnumber4000HTTP server listen port
server.basePathstring/URL prefix for all routes
server.logLevelenuminfoLog level: debug, info, warn, error
server.tlsCertPathstringPath to TLS cert (PEM) for expiry diagnostic
scaler.configPathstringPath to scalers.yaml
scaler.configDirstringPath to scalers.d/ directory

Every config field can be overridden by a KICI_-prefixed environment variable. The mapping uses underscore-separated uppercase paths:

The orchestrator holds a bounded PostgreSQL connection pool for its hot path (dispatch, heartbeat persistence, job completion). Three env vars tune it:

Env varDefaultPurpose
KICI_DB_POOL_MAX20Maximum concurrent Postgres connections. Size it to the peak concurrent hot-path operations per coordinator process.
KICI_DB_POOL_ACQUIRE_TIMEOUT_MS5000How long a caller waits for a free connection before failing fast. Prevents callers from queueing forever when the pool is saturated.
KICI_DB_STATEMENT_TIMEOUT_MS30000Aborts a single query that runs longer than this, so one runaway statement can’t hold a connection indefinitely.

Raising KICI_DB_POOL_MAX lets more hot-path operations run concurrently at the cost of more open connections against Postgres — keep the sum across all coordinator processes comfortably under the database’s max_connections.

Environment variable reference (orchestrator-specific)

Section titled “Environment variable reference (orchestrator-specific)”

Variables shared across KiCI services and the logger live in the environment variable reference. The orchestrator-specific variables, with type/default/required metadata generated from the config schema:

Env varRequiredDefaultTypeAliasesDescription
KICI_AGENT_AUTHno”token”enum:token|none
KICI_AGENT_BINARY_SOURCEnostring
KICI_AGENT_MAX_RECONNECT_DELAY_MSno60000number
KICI_AGENT_TOKEN_TTL_MSno3600000number
KICI_ARTIFACT_MAX_BYTESno1073741824numberCluster-wide default max size (bytes) of a single user-facing artifact tarball (1 GiB). A per-org override in org_settings.artifact_max_bytes (set via kici-admin org-settings artifacts set-max-bytes) takes precedence when present.
KICI_ARTIFACT_MAX_PER_RUNno50numberCluster-wide default max number of user-facing artifacts a single run may upload (50). A per-org override in org_settings.artifact_max_per_run (set via kici-admin org-settings artifacts set-max-per-run) takes precedence when present.
KICI_ARTIFACT_QUOTA_BYTESno21474836480numberCluster-wide default per-org byte quota for user-facing artifacts (ctx.artifacts). A per-org override in org_settings.artifact_quota_bytes (set via kici-admin org-settings artifacts set-quota) takes precedence when present.
KICI_ARTIFACT_TTL_MSno2592000000numberCluster-wide default per-artifact TTL (ms) for user-facing artifacts. A per-org override in org_settings.artifact_ttl_ms (set via kici-admin org-settings artifacts set-ttl) takes precedence when present.
KICI_AUTO_MIGRATEno”true”string
KICI_BACKUP_STALENESS_WARN_HOURSno24number
KICI_BASE_PATHno”/“string
KICI_BOOTSTRAP_ADMIN_TOKENnostring
KICI_CACHE_BUILD_TIMEOUT_MSno600000number
KICI_CACHE_MAX_TARBALL_BYTESno524288000number
KICI_CACHE_TTL_DAYSno30number
KICI_CHECK_RUN_TRACKING_TTL_DAYSno7number
KICI_CLUSTER_ADDRESSnostring
KICI_CLUSTER_COORDINATOR_URLnostring
KICI_CLUSTER_COORDINATOR_URLSnostring
KICI_CLUSTER_CREDENTIAL_FILEno”~/.kici/peer-credential”string
KICI_CLUSTER_ELECTION_GRACE_PERIOD_MSno60000number
KICI_CLUSTER_INSTANCE_IDno""string
KICI_CLUSTER_JOIN_TOKENnostring
KICI_CLUSTER_NAMEnostring
KICI_CLUSTER_PEER_HEARTBEAT_INTERVAL_MSno30000number
KICI_CLUSTER_PEER_MAX_RECONNECT_DELAY_MSno60000number
KICI_CLUSTER_PEER_STALE_TIMEOUT_MSno60000number
KICI_CLUSTER_PEERSnostring
KICI_CLUSTER_RAFT_ELECTION_TIMEOUT_MAX_MSno10000number
KICI_CLUSTER_RAFT_ELECTION_TIMEOUT_MIN_MSno5000number
KICI_CLUSTER_RAFT_HEARTBEAT_MSno2000number
KICI_CLUSTER_ROLEno”coordinator”enum:coordinator|worker
KICI_CLUSTER_SETTINGS_CACHE_TTL_MSno10000number
KICI_CLUSTER_SINGLE_NODEnofalseunion
KICI_CLUSTER_TRUSTED_PROXIESnostring
KICI_CONTRIBUTOR_CACHE_TTL_MSno900000number
KICI_DASHBOARD_URLnostring
KICI_DATA_DIRnostring
KICI_DATABASE_URLnostring
KICI_DB_POOL_ACQUIRE_TIMEOUT_MSno5000number
KICI_DB_POOL_MAXno20number
KICI_DB_STATEMENT_TIMEOUT_MSno30000number
KICI_DEV_IDENTITY_KEY_FILEnostring
KICI_DISPATCH_ACK_TIMEOUT_MSno10000number
KICI_DISPATCH_QUEUE_TTL_DAYSno30number
KICI_EVENT_LOG_MAX_PAYLOAD_BYTESno5242880number
KICI_EVENT_ROUTER_CLEANUP_INTERVAL_MSno3600000number
KICI_EVENT_ROUTER_EVENT_TTL_SECONDSno604800number
KICI_EVENT_ROUTER_LEASE_DURATION_MSno60000number
KICI_EVENT_ROUTER_MAX_CHAIN_DEPTHno10number
KICI_EVENT_ROUTER_MAX_DISPATCH_ATTEMPTSno5number
KICI_EVENT_ROUTER_RATE_LIMIT_PER_WORKFLOW_PER_MINUTEno100number
KICI_EVENT_ROUTER_RETRY_BASE_BACKOFF_MSno5000number
KICI_EVENT_ROUTER_RETRY_MAX_BACKOFF_MSno300000number
KICI_EVENT_ROUTER_RETRY_SCAN_INTERVAL_MSno10000number
KICI_GITHUB_APP_NAME_REFRESH_INTERVAL_MSno86400000number
KICI_HOST_REBOOT_DEADLINE_MSno900000number
KICI_INDEPENDENT_IDENTITYno”false”enum:true|false
KICI_INDEPENDENT_SECRETSno”false”enum:true|false
KICI_INGEST_CODEL_INTERVAL_MSno100number
KICI_INGEST_CODEL_TARGET_MSno50number
KICI_INGEST_LOOP_LAG_RESUME_MSno150number
KICI_INGEST_LOOP_LAG_SAMPLE_MSno100number
KICI_INGEST_LOOP_LAG_SHED_MSno200number
KICI_INGEST_MAX_CONCURRENCYno256number
KICI_INGEST_MAX_QUEUE_DEPTHno1000number
KICI_INGEST_ORG_MAX_CONCURRENCYno32number
KICI_INGEST_OVERFLOW_ENABLEDno”true”enum:true|false
KICI_INGEST_OVERFLOW_MAXno5000number
KICI_INGEST_OVERFLOW_MAX_ATTEMPTSno10number
KICI_INGEST_OVERFLOW_REPLAY_BATCHno50number
KICI_INGEST_OVERFLOW_REPLAY_INTERVAL_MSno2000number
KICI_INGEST_QUEUE_MAX_WAIT_MSno3000number
KICI_LOCK_FILE_MAX_BYTESno5242880number
KICI_LOCKFILE_CACHE_MAXno500number
KICI_LOCKFILE_CACHE_MAX_BYTESno67108864number
KICI_LOCKFILE_CACHE_TTL_MSno3600000number
KICI_LOG_STORAGE_SEGMENT_FLUSH_BYTESno1048576number
KICI_LOG_STORAGE_SEGMENT_FLUSH_MSno2000number
KICI_MACHINE_LEDGER_DIRnostring
KICI_MAX_FANOUT_HOSTSno1024number
KICI_MAX_GITHUB_PAYLOAD_BYTESno26214400number
KICI_MODEno”platform”enum:platform|hybrid|independent|observed
KICI_ORCH_RECONNECT_REPLAY_WINDOW_HOURSno24number
KICI_ORCHESTRATOR_HOST_AGENT_IDnostring
KICI_ORCHESTRATOR_KMS_ACCESS_KEY_IDnostring
KICI_ORCHESTRATOR_KMS_KEY_ARNnostring
KICI_ORCHESTRATOR_KMS_REGIONnostring
KICI_ORCHESTRATOR_KMS_SECRET_ACCESS_KEYnostring
KICI_ORCHESTRATOR_PROVENANCE_ISSUERnostring
KICI_ORCHESTRATOR_SIGNER_COMMANDnostring
KICI_ORCHESTRATOR_SIGNER_KINDnostring
KICI_ORCHESTRATOR_URLnostring
KICI_OWNERSHIP_DB_CHECK_TIMEOUT_MSno5000number
KICI_PG_CUSTOMER_SECRETSno”true”enum:true|false
KICI_PLATFORM_TOKENnostring
KICI_PLATFORM_URLnostring
KICI_PORTno4000number
KICI_PROVENANCE_ISSUERnostring
KICI_QUEUE_BACKPRESSURE_THRESHOLDno100number
KICI_QUEUE_MAX_DEPTHno1000number
KICI_QUEUE_TIMEOUT_MSno3600000number
KICI_REROUTE_ACK_TIMEOUT_MSno15000number
KICI_REROUTE_FLAP_GRACE_MSno120000number
KICI_REROUTE_MAX_HOPSno3number
KICI_REROUTE_SPAWN_WINDOW_MSno90000number
KICI_ROSTER_GRACE_MSno300000number
KICI_ROSTER_TTL_MSno1800000number
KICI_SCALER_CONFIG_DIRnostring
KICI_SCALER_CONFIG_PATHnostring
KICI_SCALER_PENDING_SWEEP_INTERVAL_MSno10000number
KICI_SCALER_SPAWN_TIMEOUT_MSno300000number
KICI_SECRET_KEYnostring
KICI_SECRET_KEY_FILEnostring
KICI_SECRET_KEY_FILE_OLDnostring
KICI_SECRET_KEY_OLDnostring
KICI_SERVER_TLS_CERT_PATHnostring
KICI_SKIP_S3_SENTINEL_VALIDATIONno”false”string
KICI_STALE_DETECTOR_SCAN_INTERVAL_MSno60000number
KICI_STALE_DETECTOR_THRESHOLD_MULTIPLIERno2number
KICI_STEP_LOG_TTL_DAYSno90number
KICI_STORAGE_BUCKETnostring
KICI_STORAGE_ENDPOINTnostring
KICI_STORAGE_EXTERNAL_ENDPOINTnostring
KICI_STORAGE_FORCE_PATH_STYLEnoenum:true|false
KICI_STORAGE_FS_BASE_URLnostring
KICI_STORAGE_FS_PATHnostring
KICI_STORAGE_LOG_BUCKETnostring
KICI_STORAGE_PATHnostring
KICI_STORAGE_PREFIXnostring
KICI_STORAGE_REGIONnostring
KICI_STORAGE_TYPEnoenum:s3|filesystem
KICI_STORAGE_UPLOAD_ENDPOINTnostring
KICI_TEST_EVENT_FAIL_FIRST_Nnostring
KICI_TEST_MINT_DEFER_AUDIENCEnostring
KICI_TEST_MINT_REJECT_AUDIENCEnostring
KICI_TEST_MODEno”0”string
KICI_TEST_OMIT_DASHBOARD_REQUEST_TYPESnostring
KICI_TEST_RERUN_DELAY_MSnonumber
KICI_UNROUTABLE_GRACE_MSno120000number
KICI_USER_CACHE_QUOTA_BYTESno5368709120numberCluster-wide default per-org byte quota for the user-facing cache (ctx.cache). A per-org override in org_settings.user_cache_quota_bytes (set via kici-admin org-settings user-cache set-quota) takes precedence when present.
KICI_USER_CACHE_TTL_MSno604800000numberCluster-wide default per-entry TTL (ms) for the user-facing cache. A per-org override in org_settings.user_cache_ttl_ms (set via kici-admin org-settings user-cache set-ttl) takes precedence when present.
KICI_WEBHOOK_DEDUP_TTL_MSno86400000number
KICI_WEBHOOK_PAYLOAD_DIRnostring
KICI_WEBHOOK_PUBLIC_URLnostring
KICI_WORKER_CONCURRENCYno5number
NODE_ENVno”development”enum:development|production|test

Not shown above: the KICI_COLD_STORE_* family (consumed directly by cold-store/orchestrator-cold-store.ts, registered in COLD_STORE_ENV_VARS so the typo catcher allows them but not part of the Zod schema). For the full storage env-var inventory plus prefix layout, see orchestrator storage layout.

Env VarConfig PathNotes
KICI_DATABASE_URLdatabase.url
KICI_SERVER_PORTserver.portCoerced to number
KICI_SERVER_BASE_PATHserver.basePath
KICI_SERVER_LOG_LEVELserver.logLevel
KICI_SERVER_TLS_CERT_PATHserver.tlsCertPathPath to TLS cert (PEM) for expiry diagnostic
KICI_INSTANCE_IDinstance.id
KICI_INSTANCE_MODEinstance.mode
KICI_SCALER_CONFIG_PATHscaler.configPath
KICI_SCALER_CONFIG_DIRscaler.configDir
KICI_PLATFORM_URLplatform.url
KICI_PLATFORM_TOKENplatform.tokenSensitive
KICI_AGENT_AUTHagentAuthDefault: token. token or none
KICI_AGENT_TOKEN_TTL_MSagentTokenTtlMsDefault: 3600000 (1h). Coerced to number
KICI_ROSTER_GRACE_MSrosterGraceMsDefault: 300000 (5m). Coerced to number. Host roster: static grace before a disconnected static host reads as unreachable. Cluster-wide default
KICI_ROSTER_TTL_MSrosterTtlMsDefault: 1800000 (30m). Coerced to number. Host roster: ephemeral GC TTL — past this a disconnected ephemeral host is reaped. Cluster-wide default
KICI_QUEUE_MAX_DEPTHqueue.maxDepthDefault: 1000. Coerced to number
KICI_QUEUE_TIMEOUT_MSqueue.timeoutMsDefault: 3600000 (1h). Coerced to number. How long a job can wait in the dispatch queue before expiring. Set to 0 for indefinite. Also configurable via admin CLI: kici-admin config set queue.timeoutMs <ms>
KICI_QUEUE_BACKPRESSURE_THRESHOLDqueue.backpressureThresholdDefault: 100. Coerced to number. Pending-depth threshold that triggers the operator-facing queue.backpressure.sustained warn log after two consecutive refresher ticks (~10s). 0 disables the warner (Prometheus kici_orch_dispatch_queue_depth gauge and Grafana panel alert continue unaffected). Also configurable via admin CLI: kici-admin config set queue.backpressureThreshold <n>
KICI_LOCKFILE_CACHE_MAXlockfileCache.maxDefault: 500. Coerced to number
KICI_LOCKFILE_CACHE_TTL_MSlockfileCache.ttlMsDefault: 3600000 (1h). Coerced to number
KICI_LOCKFILE_CACHE_MAX_BYTESlockfileCache.maxBytesDefault: 67108864 (64 MiB). Coerced to number. Bounds the lock-file cache by total bytes in addition to entry count; whichever limit trips first evicts. Cluster-wide (the cache is process-global, not per-tenant)
KICI_STALE_DETECTOR_SCAN_INTERVAL_MSstaleDetector.scanIntervalMsDefault: 60000 (1m). Coerced to number
KICI_STALE_DETECTOR_THRESHOLD_MULTIPLIERstaleDetector.thresholdMultiplierDefault: 2. Coerced to number
KICI_JOB_HEARTBEAT_INTERVAL_MSstaleDetector.heartbeatIntervalMsDefault: 60000 (1m). Coerced to number
KICI_SECRET_KEYsecrets.keySensitive
KICI_SECRET_KEY_FILEsecrets.keyFile
KICI_BOOTSTRAP_ADMIN_TOKENsecrets.bootstrapAdminTokenSensitive
KICI_WEBHOOK_PAYLOAD_DIRwebhookPayloadDirOptional. Directory path where the orchestrator fire-and-forget writes every processed webhook payload to disk as <dir>/<repoIdentifier>/<deliveryId>/payload.json. Leave unset to disable the on-disk archive.
KICI_EVENT_LOG_MAX_PAYLOAD_BYTESeventLog.maxPayloadBytesDefault: 5242880 (5 MB). Soft cap for the inbound webhook delivery log (event_log table). Oversized payloads are recorded with payload_omitted=true rather than 413’d; the metadata + hash + size are still durable. Payloads below the cap are gzipped + uploaded to the existing LogStorage adapter at event-log/<orgId>/<deliveryId>.json.gz. Row retention is managed by the cold-store sweeper (see KICI_COLD_STORE_EVENT_LOG_* env vars) rather than a separate retention window.
KICI_CACHE_TTL_DAYScacheTtlDaysDefault: 30. Coerced to number
KICI_CACHE_BUILD_TIMEOUT_MScacheBuildTimeoutMsDefault: 600000 (10m). Coerced to number
KICI_CACHE_MAX_TARBALL_BYTEScacheMaxTarballBytesDefault: 524288000 (500MB). Coerced to number
KICI_USER_CACHE_QUOTA_BYTESuserCacheQuotaBytesDefault: 5368709120 (5 GiB). Coerced to number. Cluster-wide default per-org byte quota for the user-facing cache (ctx.cache); a per-org override in org_settings.user_cache_quota_bytes takes precedence when present
KICI_USER_CACHE_TTL_MSuserCacheTtlMsDefault: 604800000 (7d). Coerced to number. Cluster-wide default per-entry TTL for the user-facing cache; a per-org override in org_settings.user_cache_ttl_ms takes precedence when present
KICI_STORAGE_TYPEstorage.types3 or filesystem. s3 additionally requires KICI_STORAGE_BUCKET; filesystem requires an absolute KICI_STORAGE_FS_PATH
KICI_STORAGE_BUCKETstorage.bucket
KICI_STORAGE_PREFIXstorage.prefix
KICI_STORAGE_REGIONstorage.region
KICI_STORAGE_ENDPOINTstorage.endpoint
KICI_STORAGE_EXTERNAL_ENDPOINTstorage.externalEndpoint
KICI_STORAGE_FORCE_PATH_STYLEstorage.forcePathStyleCoerced to boolean
KICI_STORAGE_LOG_BUCKETstorage.logBucket
KICI_PG_CUSTOMER_SECRETSpgCustomerSecretsCoerced to boolean. Default: true
KICI_CLUSTER_JOIN_TOKENcluster.joinTokenSensitive, one-time use for first join
KICI_CLUSTER_CREDENTIAL_FILEcluster.credentialFileDefault: ~/.kici/peer-credential
KICI_CLUSTER_AUTO_ROTATE_CREDENTIALScluster.autoRotateCredentialsCoerced to boolean. Default: false
KICI_CLUSTER_ADDRESScluster.address
KICI_CLUSTER_INSTANCE_IDcluster.instanceId
KICI_CLUSTER_PEERScluster.peersComma-separated
KICI_CLUSTER_RAFT_ELECTION_TIMEOUT_MIN_MScluster.raftElectionTimeoutMinMsDefault: 5000. Coerced to number
KICI_CLUSTER_RAFT_ELECTION_TIMEOUT_MAX_MScluster.raftElectionTimeoutMaxMsDefault: 10000. Coerced to number
KICI_CLUSTER_RAFT_HEARTBEAT_MScluster.raftHeartbeatMsDefault: 2000. Coerced to number
KICI_CLUSTER_PEER_HEARTBEAT_INTERVAL_MScluster.peerHeartbeatIntervalMsDefault: 30000 (30s). Coerced to number
KICI_CLUSTER_PEER_MAX_RECONNECT_DELAY_MScluster.peerMaxReconnectDelayMsDefault: 60000 (1m). Coerced to number
KICI_CLUSTER_ROLEcluster.roleDefault: coordinator. coordinator or worker
KICI_CLUSTER_COORDINATOR_URLcluster.coordinatorUrlWorkers require this or KICI_CLUSTER_COORDINATOR_URLS
KICI_CLUSTER_COORDINATOR_URLScluster.coordinatorUrlsComma-separated. Multi-coordinator worker: connects to every listed coordinator; takes precedence over KICI_CLUSTER_COORDINATOR_URL when both are set
KICI_CLUSTER_PEER_STALE_TIMEOUT_MScluster.peerStaleTimeoutMsDefault: 60000 (1m). Coerced to number
KICI_EVENT_ROUTER_MAX_CHAIN_DEPTHeventRouter.maxChainDepthDefault: 10. Coerced to number. Maximum depth for chained event routing
KICI_EVENT_ROUTER_RATE_LIMIT_PER_WORKFLOW_PER_MINUTEeventRouter.rateLimitPerWorkflowPerMinuteDefault: 100. Coerced to number. Rate limit per workflow per minute for event routing
KICI_EVENT_ROUTER_EVENT_TTL_SECONDSeventRouter.eventTtlSecondsDefault: 604800 (7d). Coerced to number. Time-to-live for routed events
KICI_EVENT_ROUTER_CLEANUP_INTERVAL_MSeventRouter.cleanupIntervalMsDefault: 3600000 (1h). Coerced to number. Interval between expired event cleanup sweeps
KICI_LOG_LEVELlogLevelDefault: info
KICI_NODE_ENVnodeEnvDefault: development

For multi-provider setups, env vars use the app name from the config (hyphen to underscore, uppercased):

KICI_PROVIDERS_GITHUB_<APP_NAME>_<FIELD>
Env Var PatternConfig PathExample
KICI_PROVIDERS_GITHUB_<NAME>_APP_IDproviders.github[name].appIdKICI_PROVIDERS_GITHUB_MAIN_ORG_APP_ID=12345
KICI_PROVIDERS_GITHUB_<NAME>_PRIVATE_KEYproviders.github[name].privateKeyKICI_PROVIDERS_GITHUB_MAIN_ORG_PRIVATE_KEY=...
KICI_PROVIDERS_GITHUB_<NAME>_WEBHOOK_SECRETproviders.github[name].webhookSecretKICI_PROVIDERS_GITHUB_MAIN_ORG_WEBHOOK_SECRET=whsec_...

App name conversion: YAML main-org becomes env var segment MAIN_ORG (hyphen to underscore, uppercased).

If an env var references an app name that does not exist in the config, a stub app entry is created automatically.

Important: Legacy provider-specific env vars without the KICI_ prefix (e.g., PROVIDERS_GITHUB_APP_ID, PROVIDERS_GITHUB_PRIVATE_KEY, GITHUB_APP_ID) are not recognized. Only NODE_ENV is still honored as a low-priority unprefixed fallback; every other config field requires its KICI_-prefixed env var name.

The full resolution chain is: env var > local YAML > shared DB > defaults.

Example: Suppose the queue max depth is set in three places:

Listed highest-priority first:

SourceValue
Env var KICI_QUEUE_MAX_DEPTH2000
Local YAML(not set)
Shared DB config500
Built-in default1000

The resolved value is 2000 (env var wins). If the env var were absent, it would be 500 (DB wins over default). If the DB config were also absent, it would be 1000 (default).

The orchestrator caps how many webhook-ingest pipelines run concurrently so a burst of inbound deliveries (a backlog replay, a noisy source, or a sudden fan-in) can never saturate the event loop, the provider-API budget, or the database pool. Admission runs before any pipeline work, on both ingress paths: the HTTP direct-ingress routes and the Platform-relayed deliveries.

Admission is evaluated in three layers:

  1. Event-loop-lag gate. A background sampler tracks the event-loop delay p99. When it crosses the shed threshold the gate opens and new deliveries are shed until the delay recovers below the resume threshold (a hysteresis band prevents flapping). This is the adaptive signal — it measures the real symptom (loop starvation) and needs no database access, so it stays healthy exactly when the database is the bottleneck.
  2. Concurrency caps. A global in-flight backstop plus a per-org fairness cap, so one noisy tenant cannot starve others. If resolving the org for a delivery is slow (a cold cache against a stressed database), admission degrades gracefully to a per-source key with the cluster default rather than waiting on the database.
  3. Controlled-delay queue (HTTP direct-ingress only). A short bounded queue absorbs transient bursts; a sustained standing queue fails fast rather than adding latency to deliveries that will be rejected anyway. Platform-relayed deliveries never queue — they are granted immediately or shed — because the relay awaits the response synchronously against a tight deadline.

When admission sheds a delivery, the orchestrator answers HTTP 429 with a Retry-After header. The 429 contract is unconditional — the caller can always redeliver:

  • GitHub App sources redeliver failed webhook deliveries automatically within a bounded backoff window, so a shed delivery is retried without operator action.
  • Generic webhook senders SHOULD implement their own retry (honoring Retry-After).

On top of the 429 contract, the orchestrator additively persists every shed delivery to a durable overflow buffer (a PostgreSQL table) and replays it back through normal ingest once capacity recovers. This means a burst past the ingest limit is not dropped even if the sender never retries — the shed still returns 429, and the delivery is also captured for automatic replay. The two paths are safe together: the sender’s own redelivery and the orchestrator’s replay both flow through the delivery-id dedup, so they collapse to a single processed delivery.

Behavior:

  • Bounded, with a lossy fallback at the cap. The buffer holds at most KICI_INGEST_OVERFLOW_MAX rows (default 5000). At the cap the delivery is dropped from the buffer (never unbounded storage) — the 429 still stands, so a retrying sender is still covered.
  • Replays only once capacity recovers. A background replayer drains the oldest buffered deliveries first, and only while the orchestrator is not actively shedding on event-loop lag — replaying into a still-overloaded orchestrator would just re-shed. A delivery that re-sheds on replay is returned to the buffer (never lost) and retried; past KICI_INGEST_OVERFLOW_MAX_ATTEMPTS attempts it is marked failed and kept for inspection.
  • Idempotent by delivery id. Replay re-injects through the same admission-gated pipeline, so the delivery-id dedup guarantees a replayed delivery is never double-dispatched.
  • Best-effort FIFO ordering. Deliveries replay oldest-first by capture time. Strict cross-delivery ordering is not guaranteed when replayed traffic interleaves with live traffic — acceptable because webhook processing is already unordered across deliveries.

Set KICI_INGEST_OVERFLOW_ENABLED=false to disable capture entirely (a shed then only returns 429). The replay pacing is tuned by KICI_INGEST_OVERFLOW_REPLAY_INTERVAL_MS and KICI_INGEST_OVERFLOW_REPLAY_BATCH.

The cluster-wide defaults are generous, so admission is a no-op under normal traffic and only tightens under real pressure. The knobs are the KICI_INGEST_* environment variables in the reference table above: the global concurrency backstop, queue depth, controlled-delay target/interval, the queue wait ceiling, the loop-lag shed/resume thresholds, the sample interval, and the per-org concurrency cluster default.

Per-org concurrency cap (cluster-configurable)

Section titled “Per-org concurrency cap (cluster-configurable)”

The per-org fairness cap is a cluster-configurable setting: KICI_INGEST_ORG_MAX_CONCURRENCY sets the cluster default, and an operator can override it per organization at runtime without redeploying:

Terminal window
# Show the current per-org cap (or "(cluster default)" when unset)
kici-admin org-settings ingest-concurrency show --org <customer-id>
# Set a per-org cap
kici-admin org-settings ingest-concurrency set 16 --org <customer-id>
# Clear the override (fall back to the cluster default)
kici-admin org-settings ingest-concurrency reset --org <customer-id>

The orchestrator exposes admission-control state on its Prometheus /metrics endpoint: kici_orch_ingest_inflight, kici_orch_ingest_queue_depth, kici_orch_ingest_event_loop_delay_p99_ms / _max_ms, kici_orch_ingest_shedding_active (1 when the delay circuit breaker is engaged), the kici_orch_ingest_admitted_total and kici_orch_ingest_shed_total{reason} counters, and the config-sourced limit gauges (kici_orch_ingest_max_concurrency, kici_orch_ingest_max_queue_depth, kici_orch_ingest_org_max_concurrency, kici_orch_ingest_loop_lag_shed_ms, kici_orch_ingest_loop_lag_resume_ms) so a dashboard can plot current usage against the deployed caps.

The durable overflow buffer adds four series: kici_orch_ingest_overflow_buffered (gauge — current buffered-row depth awaiting replay), kici_orch_ingest_overflow_captured_total (deliveries captured into the buffer), kici_orch_ingest_overflow_replayed_total (deliveries successfully replayed), and kici_orch_ingest_overflow_dropped_total{reason} (deliveries permanently dropped, reason being cap_full when the buffer was at its cap or max_attempts when replay exhausted its retries).

The orchestrator supports multiple webhook sources simultaneously — GitHub App sources and generic webhook sources alike. Each is managed as a webhook source via the kici-admin source commands (not through config YAML or config seed).

Terminal window
# Add a GitHub App source
kici-admin source add github \
--name main-org \
--app-id 12345 \
--private-key @main-org.pem \
--webhook-secret whsec_main_secret
# Add another app
kici-admin source add github \
--name partner-org \
--app-id 67890 \
--private-key @partner-org.pem \
--webhook-secret whsec_partner_secret

See docs/operator/orchestrator/kici-admin-cli.md for the full source command reference.

Each app registers its own routing key (e.g., github:12345, github:67890) with the Platform relay via source.register messages. The ProviderRegistry maps each routing key to its own provider bundle (normalizer, lock file fetcher, clone token provider, etc.).

One HTTP listener for every source (no per-source ports)

Section titled “One HTTP listener for every source (no per-source ports)”

Every webhook source — GitHub Apps, generic webhooks, internal sources — is served from the single HTTP listener the orchestrator binds at startup. The listener address is controlled by KICI_PORT (one numeric value, no list, no per-source override) and the orchestrator routes inbound deliveries by path, not by port:

  • POST /webhook/:orgId/github — every GitHub App source registered on this orchestrator
  • POST /webhook/:orgId/generic/:sourceId — one path per generic source, distinguished by the sourceId segment

The generic_webhook_sources table has no port column, and kici-admin source add ... exposes no --port flag — there is intentionally no way to give one source its own listener while another stays on KICI_PORT. If you want different upstream URLs per source (different hostnames, different TLS certs, different ingress paths), terminate that distinction at your reverse proxy / load balancer and forward all of them to the orchestrator’s single port. The same KICI_BASE_PATH reverse-proxy pattern documented in getting-started is the supported way to host the orchestrator behind a custom URL prefix.

The KICI_PROVIDERS_GITHUB_<NAME>_<FIELD> env vars (documented in the env var table above) can still inject provider fields into the runtime config object, but the primary mechanism for managing sources is kici-admin source add.

The orchestrator writes to three independent object-storage subsystems (cache, logs, cold-store). The full bucket-and-prefix map — including which env var names which bucket, what data lives under each prefix, and per-table cold-store tuning — lives in storage layout. Two storage-specific quirks worth knowing up front:

Cache storage env vars are KICI_STORAGE_*. The orchestrator reads the KICI_STORAGE_TYPE / KICI_STORAGE_BUCKET / KICI_STORAGE_PREFIX / KICI_STORAGE_REGION / KICI_STORAGE_ENDPOINT / KICI_STORAGE_EXTERNAL_ENDPOINT / KICI_STORAGE_FORCE_PATH_STYLE / KICI_STORAGE_LOG_BUCKET family directly via loadConfig() in packages/orchestrator/src/config.ts and bridges them into the storage.* config field. The names follow the project-wide KICI_-prefix convention and benefit from the unknown-env-var typo catcher at boot.

The log-storage prefix is hardcoded. Step logs are written under kici-logs/... and webhook payloads under event-log/{orgId}/{deliveryId}.json.gz — neither is configurable via env var. If you need a different layout (e.g., to share the log bucket with another service that already owns one of these prefixes), use KICI_STORAGE_LOG_BUCKET to point logs at a dedicated bucket rather than trying to relocate the prefix.

The orchestrator reports how it was deployed so the dashboard’s infrastructure page can show the correct, copy-ready kici-admin invocation for each orchestrator. Three env vars carry this:

VariableValuesWhen set
KICI_DEPLOY_MODEsystemd | launchd | windows | composealways, for an installed orchestrator
KICI_DEPLOY_CONTAINERthe container namecontainer (compose) deployments only
KICI_DEPLOY_CONTAINER_RUNTIMEpodman | dockercontainer (compose) deployments only

You normally don’t set these by hand. kici-admin orchestrator install writes them into the orchestrator’s env file automatically based on the deployment shape it just created — a systemd / launchd / Windows-service install writes KICI_DEPLOY_MODE alone; a container (compose) install also writes the container name and runtime so the dashboard can render the <runtime> exec <container> kici-admin … form. A hand-run orchestrator (no installer) reports an unknown shape, and the dashboard falls back to a bare kici-admin command plus a note to set KICI_ADMIN_URL / KICI_ADMIN_TOKEN. The values follow the project-wide KICI_-prefix convention; the orchestrator reads them directly at startup and they are exempt from the unknown-env-var typo catcher. Surrounding whitespace is ignored on all three, so a hand-edited env file that leaves a stray space or a trailing newline on a value still reports the right shape. Any other unrecognized KICI_DEPLOY_MODE value reports an unknown shape; an unrecognized container runtime is simply omitted, leaving the mode intact.

Secrets stored in the shared DB config (private keys, tokens, webhook secrets) are encrypted at rest using AES-256-GCM. The encryption key is derived from a master key that must be available on every orchestrator instance.

Set the master key via:

  • Env var: KICI_SECRET_KEY (64-character hex string or base64-encoded)
  • File: KICI_SECRET_KEY_FILE — path to a file containing the key. The orchestrator reads the file at startup.

The master key is the minimum bootstrap secret — the only secret that must be distributed out-of-band to each orchestrator. All other secrets can then be stored encrypted in the database.

When you seed config to the database (kici-admin config seed), the following fields are automatically encrypted before storage:

  • platform.token
  • secrets.key
  • secrets.bootstrapAdminToken
  • cluster.joinToken

Provider secrets (privateKey, webhookSecret) are not part of the config system. They are stored separately via the PgSecretStore in the secrets table, managed through the sources API.

Each encrypted field uses a path-specific AAD (Additional Authenticated Data) in the format config-field:<path>, binding the ciphertext to its specific location in the config tree. The encrypted_paths array is stored alongside each config version so the system knows exactly which fields to decrypt on read.

When you query config via the admin API or CLI (kici-admin config get), sensitive values in the response are redacted as ***REDACTED***.

The auto-scaler configuration (scalers.yaml) remains a separate file, not part of the YAML/DB config system. It is referenced from the local config via scaler.configPath and scaler.configDir.

When SIGHUP is sent to the orchestrator, both the orchestrator config and the scaler config are reloaded together (unified signal). See Auto-scaler configuration for the scaler YAML schema and examples.

On startup, the orchestrator validates the merged config against appConfigSchema (Zod). If validation fails, the service prints all errors and exits:

Configuration validation failed:
- platformUrl: platformUrl is required when mode is platform/hybrid/observed
- platformToken: platformToken is required when mode is platform/hybrid/observed
  • Worker mode requires a coordinator URL: If cluster.role is worker, either cluster.coordinatorUrl or the env-only multi-coordinator list KICI_CLUSTER_COORDINATOR_URLS (comma-separated; takes precedence over the singular form when both are set) must be present
  • Coordinator mode requires database: databaseUrl is required when cluster.role is coordinator (the default). Workers do not need a database connection.
  • Platform-connected modes: platformUrl and platformToken are required when the mode is platform, hybrid, or observed (skipped for workers). independent is the only mode that never holds a Platform connection.
  • Observed mode requires a public webhook URL: If the mode is observed, KICI_WEBHOOK_PUBLIC_URL must be set — the orchestrator serves its own ingress, so it must advertise the base URL providers post to (skipped for workers)
  • Cluster peers require address: If cluster.peers is set, cluster.address is required
  • S3 storage requires bucket: If storage.type is s3, storage.bucket is required
  • Filesystem storage requires an absolute path: If KICI_STORAGE_TYPE is filesystem, KICI_STORAGE_FS_PATH must be set to an absolute path (see storage layout for the filesystem backend)

Validate a YAML file without contacting the orchestrator:

Terminal window
kici-admin config validate --file orchestrator.yaml --type local --offline
kici-admin config validate --file shared-config.yaml --type shared --offline

Each orchestrator instance generates a unique instanceId at startup using a random UUID (e.g., f47ac10b-58cc-4372-a567-0e02b2c3d479). Override with instance.id in YAML or KICI_INSTANCE_ID env var.

orchestrator.yaml:

database:
url: 'postgresql://kici:s3cur3pa55w0rd@postgres:5432/kici'
instance:
mode: 'platform'
server:
port: 4000
logLevel: 'info'

Env vars:

Terminal window
KICI_PROVIDERS_GITHUB_MAIN_ORG_APP_ID=123456
KICI_PROVIDERS_GITHUB_MAIN_ORG_PRIVATE_KEY="$(cat /keys/private-key.pem)"
KICI_PROVIDERS_GITHUB_MAIN_ORG_WEBHOOK_SECRET=whsec_github_secret
KICI_PLATFORM_URL=wss://api.kici.dev/ws
KICI_PLATFORM_TOKEN=kici_abc123def456
KICI_SECRET_KEY=<64-char-hex-master-key>
KICI_BOOTSTRAP_ADMIN_TOKEN=<admin-token>

orchestrator.yaml:

database:
url: 'postgresql://kici:s3cur3pa55w0rd@postgres:5432/kici'
instance:
mode: 'independent'
server:
port: 4000

Observed Mode (own ingress, hosted observability)

Section titled “Observed Mode (own ingress, hosted observability)”

Webhooks are posted straight to the orchestrator’s own public URL and never transit KiCI, but the orchestrator keeps its Platform connection so runs, jobs, steps, logs, and events show up in the hosted dashboard. Its sources are recorded as observe-only: dashboard-visible, never routed.

orchestrator.yaml:

database:
url: 'postgresql://kici:s3cur3pa55w0rd@postgres:5432/kici'
instance:
mode: 'observed'
server:
port: 4000

Plus the environment the mode requires:

Terminal window
KICI_PLATFORM_URL=wss://api.kici.dev/ws
KICI_PLATFORM_TOKEN=kici_ok_...
KICI_WEBHOOK_PUBLIC_URL=https://kici.example.com

KICI_WEBHOOK_PUBLIC_URL is mandatory in this mode — the orchestrator serves its own ingress, so it must advertise the base URL providers post to. GitHub-App sources are refused (both at startup and by kici-admin source add) because they are ingested through the Platform relay; use a generic or local source, or switch to hybrid if you want the relay.

orchestrator.yaml:

database:
url: 'postgresql://kici:s3cur3pa55w0rd@postgres:5432/kici'
instance:
mode: 'hybrid'
server:
port: 4000

Shared config (seeded to DB):

platform:
url: 'wss://api.kici.dev/ws'
storage:
type: 's3'
bucket: 'kici-cache'

Provider credentials (GitHub App private keys, webhook secrets) are not part of the shared config schema. Manage them with kici-admin source add instead — see Multi-Provider Setup.

Mode / RoleEntry PointCMD Override Needed
platformserver.js (default)No
hybridserver.js (default)No
observedserver.js (default)No
independentstandalone.jsYes: node packages/orchestrator/dist/standalone.js
cluster.role = workerserver.js (default)No (workers bypass mode check, work from any entry)