Nimbusv1.0.0

Events

Every event type emitted by the Nimbus controller, grouped by subsystem.

Events are sealed subclasses of NimbusEvent (see nimbus-core/src/main/kotlin/dev/nimbuspowered/nimbus/event/Events.kt). They are emitted on the internal EventBus and — after running through EventRoutes.toEventMessage() — serialised into the wire format pushed over /api/events and stored in the audit log.

Wire format

Every frame over WebSocket is a single JSON object:

{
  "type": "SERVICE_READY",
  "timestamp": "2026-04-15T12:34:56.123Z",
  "data": { "service": "Lobby-1", "group": "Lobby" }
}
  • type is the stable uppercase type string from the table below.
  • timestamp is the event's Instant serialised as an ISO-8601 string (UTC).
  • data is a flat { string: string } map. Numbers and booleans are stringified — consumers must parse them back.

The actor field

Every NimbusEvent carries an actor: String field (default "system", settable before emit). Common values:

ActorMeaning
systemController-initiated (scheduler, scaler, watchdog).
consoleLocal JLine console operator.
api:adminAdmin-token API caller.
api:serviceService-token API caller (SDK, Bridge).

actor is used by AuditCollector for the audit_log table. It is not currently included in the WebSocket payload — the actor visible in the audit log does not round-trip through /api/events today.


Service lifecycle

ClassTypedata keys
ServiceStartingSERVICE_STARTINGservice, group, port
ServiceReadySERVICE_READYservice, group
ServiceDrainingSERVICE_DRAININGservice, group
ServiceStoppingSERVICE_STOPPINGservice
ServiceStoppedSERVICE_STOPPEDservice
ServiceCrashedSERVICE_CRASHEDservice, exitCode, restartAttempt
ServiceRecoveredSERVICE_RECOVEREDservice, group, pid, port — emitted when the watchdog re-attaches to a running PID after controller restart
ServicePreparedSERVICE_PREPAREDservice, group — warm-pool slot is staged and waiting
ServiceDeployedSERVICE_DEPLOYEDservice, group, filesChanged — deploy-on-stop finished copying changes back to the template
ServiceCustomStateChangedSERVICE_CUSTOM_STATE_CHANGEDservice, group, optional oldState, optional newState
WarmPoolReplenishedWARM_POOL_REPLENISHEDgroup, poolSize

SERVICE_READY is the point at which service.ready() pattern matched in stdout (or the SDK plugin reported ready). Use it rather than SERVICE_STARTING for "is available to players" logic.


Scaling & placement

ClassTypedata keys
ScaleUpSCALE_UPgroup, from, to, reason
ScaleDownSCALE_DOWNgroup, service, reason — note: single service name, no from/to
PlacementBlockedPLACEMENT_BLOCKEDgroup, reason — pinned node unavailable, etc.

State sync

ClassTypedata keys
SyncCompletedSYNC_COMPLETEDservice, filesInManifest, filesReceived, bytesReceived, durationMs
SyncFailedSYNC_FAILEDservice, reason

Players

ClassTypedata keys
PlayerConnectedPLAYER_CONNECTEDplayer, uuid, service
PlayerDisconnectedPLAYER_DISCONNECTEDplayer, uuid, service
PlayerServerSwitchPLAYER_SERVER_SWITCHplayer, uuid, from, to

These are emitted when the Bridge posts to POST /api/proxy/events. The Players module subscribes here to update the tracker.


Groups

ClassTypedata keys
GroupCreatedGROUP_CREATEDgroup
GroupUpdatedGROUP_UPDATEDgroup
GroupDeletedGROUP_DELETEDgroup

Service messaging

ClassTypedata keys
ServiceMessageSERVICE_MESSAGEfrom, to, channel, plus every entry of the user-supplied data map merged in at the top level

Emitted by POST /api/services/{name}/message. The target name controller is a virtual sink — the event fires without requiring a registered service.


Proxy sync

ClassTypedata keys
TabListUpdatedTABLIST_UPDATEDheader, footer, playerFormat, updateInterval
MotdUpdatedMOTD_UPDATEDline1, line2, maxPlayers, playerCountOffset
PlayerTabUpdatedPLAYER_TAB_UPDATEDuuid, optional format (absent when cleared)
ChatFormatUpdatedCHAT_FORMAT_UPDATEDformat, enabled

Maintenance

ClassTypedata keys
MaintenanceEnabledMAINTENANCE_ENABLEDscope ("global" or group name), optional reason (omitted when blank)
MaintenanceDisabledMAINTENANCE_DISABLEDscope

Cluster

ClassTypedata keys
ClusterStartedCLUSTER_STARTEDbind, port, strategy
NodeConnectedNODE_CONNECTEDnodeId, host
NodeDisconnectedNODE_DISCONNECTEDnodeId
NodeHeartbeatNODE_HEARTBEATnodeId, cpuUsage, services

NODE_HEARTBEAT is high-frequency and is suppressed on /api/console/stream. Subscribe to /api/events directly if you need it.


Load balancer

ClassTypedata keys
LoadBalancerStartedLOAD_BALANCER_STARTEDbind, port, strategy
LoadBalancerStoppedLOAD_BALANCER_STOPPEDreason
LoadBalancerBackendHealthChangedLOAD_BALANCER_BACKEND_HEALTH_CHANGEDhost, port, oldStatus, newStatus

Dedicated

ClassTypedata keys
DedicatedCreatedDEDICATED_CREATEDname
DedicatedDeletedDEDICATED_DELETEDname

Stress test

ClassTypedata keys
StressTestUpdatedSTRESS_TEST_UPDATEDsimulatedPlayers, targetPlayers, optional targetGroup, optional perService ("name=count,name=count" when non-empty)

Fires every tick of a running stress test — also suppressed on /api/console/stream.


Config / API / Updates

ClassTypedata keys
ConfigReloadedCONFIG_RELOADEDgroupsLoaded
ApiStartedAPI_STARTEDbind, port
ApiStoppedAPI_STOPPEDreason
ApiWarningAPI_WARNINGmessage
ApiErrorAPI_ERRORerror
NimbusUpdateAvailableNIMBUS_UPDATE_AVAILABLEcurrentVersion, newVersion, updateType (MAJOR/MINOR/PATCH)
NimbusUpdateAppliedNIMBUS_UPDATE_APPLIEDoldVersion, newVersion
ProxyUpdateAvailablePROXY_UPDATE_AVAILABLEcurrentVersion, newVersion
ProxyUpdateAppliedPROXY_UPDATE_APPLIEDoldVersion, newVersion

Module lifecycle

ClassTypedata keys
ModuleLoadedMODULE_LOADEDmoduleId, moduleName, moduleVersion
ModuleEnabledMODULE_ENABLEDmoduleId, moduleName
ModuleDisabledMODULE_DISABLEDmoduleId, moduleName

Remote CLI sessions

ClassTypedata keys
CliSessionConnectedCLI_SESSION_CONNECTEDsessionId, remoteIp, clientUsername, clientHostname, clientOs, location
CliSessionDisconnectedCLI_SESSION_DISCONNECTEDsessionId, remoteIp, clientUsername, durationSeconds, commandCount

Rows are also written to the cli_sessions table so the sessions console command can show historical connections.


MODULE_EVENT envelope — module-emitted events

Important semantic. Controller modules do not own top-level sealed subclasses of NimbusEvent. Instead they emit NimbusEvent.ModuleEvent(moduleId, type, data) — but the serializer unwraps this and sends the envelope's type directly at the top level, with moduleId folded into the data map where relevant. That's why PERMISSION_GROUP_CREATED, PUNISHMENT_ISSUED, BACKUP_COMPLETED etc. appear as first-class type strings on the wire, even though they are dispatched through ModuleEvent internally.

Module events below use the same frame format. moduleId is included so generic subscribers can filter by owning module without pattern-matching on the type.

Permissions module (moduleId = "perms")

Typedata keys
PERMISSION_GROUP_CREATEDgroup
PERMISSION_GROUP_UPDATEDgroup
PERMISSION_GROUP_DELETEDgroup
PLAYER_PERMISSIONS_UPDATEDuuid, player
PERMISSION_TRACK_CREATEDtrack
PERMISSION_TRACK_DELETEDtrack
PLAYER_PROMOTEDuuid, player, track, newGroup
PLAYER_DEMOTEDuuid, player, track, newGroup

Punishments module (moduleId = "punishments")

Typedata keys
PUNISHMENT_ISSUEDid, type (BAN/TEMPBAN/IPBAN/MUTE/TEMPMUTE/KICK/WARN), target, targetUuid, issuer, reason, expiresAt (empty string when permanent), scope (NETWORK/GROUP/SERVICE), optional scopeTarget, optional kickMessage (pre-rendered)
PUNISHMENT_REVOKEDid, type, target, revokedBy, reason
PUNISHMENT_EXPIREDid, type, target — emitted by the 30 s expiry loop

Resource packs module (moduleId = "resourcepacks")

Typedata keys
RESOURCE_PACK_CREATEDid, name, source (URL/LOCAL)
RESOURCE_PACK_DELETEDid, name
RESOURCE_PACK_ASSIGNEDpackId, scope (GLOBAL/GROUP/SERVICE), target
RESOURCE_PACK_UNASSIGNEDpackId, scope, target
RESOURCE_PACK_STATUSplayer, pack, status — backend telemetry (ACCEPTED/DECLINED/LOADED/FAILED_DOWNLOAD, etc.)

Backup module (moduleId = "backup")

Typedata keys
BACKUP_STARTEDid, targetType, targetName, triggeredBy
BACKUP_COMPLETEDid, targetName, sizeBytes, durationMs, status (SUCCESS / PARTIAL)
BACKUP_FAILEDid, targetName, reason
BACKUP_RESTOREDid, targetName, targetPath, triggeredBy
BACKUP_PRUNEDcount, freedBytes, scheduleClass

BACKUP_COMPLETED with status = "PARTIAL" is emitted when some target (e.g. a remote-node service, or a missing mysqldump binary) was skipped with a WARN but the archive was written successfully. Treat PARTIAL as "inspect the record details" rather than "failure".

Scaling module (moduleId = "scaling")

Typedata keys
SMART_SCHEDULEgroup, rule, started, min — a time-based schedule rule started services
SMART_WARMUPgroup, rule, started, min — warmup from the next upcoming rule
SMART_PREDICTIONgroup, predicted, started, samples — predictive warmup from player-count history

Every decision is also persisted to the scaling_decisions table (exposed via GET /api/scaling/decisions).


Event subscription

  • WebSocket — connect to /api/events with a bearer token. Every event shows up as one frame. See WebSocket reference.
  • Remote CLI / multiplex — subscribers to /api/console/stream receive events inside { "type": "event", "event": { ... } } envelopes. STRESS_TEST_UPDATED and NODE_HEARTBEAT are suppressed here to keep the CLI readable.
  • Audit logAuditCollector persists a filtered subset of events (actor-attributable ones) to the audit_log table with the actor field preserved. Query with GET /api/audit.
  • In-process — modules and core subsystems subscribe via EventBus.subscribe() and receive the raw sealed-class instances, including the full actor field and strongly-typed properties.

Firing custom events

The MODULE_EVENT envelope is the sanctioned way for a 3rd-party module to push its own events without forking Events.kt. Use:

eventBus.emit(NimbusEvent.ModuleEvent(
    moduleId = "my-module",
    type = "MY_THING_HAPPENED",
    data = mapOf("target" to name, "reason" to reason)
))

The type string is what external subscribers see in the top-level type field, so pick something stable and uppercase. Include moduleId in the data too if you want receivers to filter without namespacing the type.