Changelog
All notable changes to Nimbus, organized by release.
All notable changes to this project are documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Nimbus follows Semantic Versioning. From 1.0.0 onwards the REST API has a stability guarantee — breaking changes only in major versions. See API Stability for the full policy.
The Web Dashboard is versioned independently and has its own release history — see the Dashboard Changelog.
[1.0.0] - 2026-04-24
First stable release. No new features — this version exists to declare that the core engine, module framework, REST API, cluster protocol, and all first-party modules are production-ready and covered by SemVer stability guarantees going forward.
Changed
[controller] strict_configdefault flipped totrue— unknown keys innimbus.toml, group/dedicated configs, and module configs are now a startup-aborting error rather than a warning. Operators who added unrecognised keys to silence deprecation warnings in 0.13.x need to clean them up before upgrading. The deprecated compatibility shims removed in 0.14.0 are not present in 1.0.0 config files.nimbus.playerspermission node removed — the auto-migration that addednimbus.cloud.playersalongside the old node (introduced in 0.13.0) now only writes the new node. Groups that still referencenimbus.playersdirectly will no longer match.- Module API compat shims removed — the
@Deprecatedtype aliases indev.nimbuspowered.nimbus.modulepointing todev.nimbuspowered.nimbus.module.apiare gone. Third-party modules that haven't migrated their imports will fail to compile against 1.0.0.
Notes
- The REST API (
/api/*) is now stable. Additive changes (new fields, new endpoints) happen in minor versions. Removals and semantic changes happen in major versions with a documented migration guide. See API Stability. - Cluster protocol
protocolVersion = 1(unchanged from 0.13.0). - All first-party modules ship at 1.0.0 alongside the controller; their APIs follow the same stability policy.
[0.14.0] - 2026-04-24
Cleanup release focused on removing the deprecated compatibility shims introduced in 0.13.0, now that operators have had time to migrate. No new features; no behaviour changes for operators who followed the 0.13.0 migration guide.
Removed
dev.nimbuspowered.nimbus.module.*type aliases — the@Deprecatedforwarding aliases (NimbusModule,ModuleContext,ModuleCommand,AuthPrincipal, and all other public module API types) that pointed to theirdev.nimbuspowered.nimbus.module.api.*counterparts are removed. Third-party module authors must update their imports to themodule.apipackage before upgrading to 0.14.0.object ApiErrorscompat facade — the deprecatedApiErrorsstring-constant object that delegated to theApiErrorenum is removed. Any code referencingApiErrors.*directly must switch toApiError.*or the typed enum values.nimbus.playerspermission node support — the old node no longer triggers a match in the permission engine. Any groups still referencing it should be updated tonimbus.cloud.players.
Migration
If you are upgrading from 0.13.x: follow the migration notes from the 0.13.0 release first (package rename, permission node rename). No additional operator-facing config changes are required for 0.14.0 — the breaking changes listed above only affect third-party module developers and direct API consumers using the internal ApiErrors constants.
[0.13.0] - 2026-04-21
Added
- Strict TOML validation (
[controller] strict_config) — controller and module configurations (nimbus.toml,groups/*.toml,dedicated/*.toml,config/modules/syncproxy/{motd,tablist,chat}.toml,config/modules/auth/auth.toml,config/modules/docker/docker.toml,agent.toml) now log a separate WARN line for every unknown key at startup, including the key name, scope, and a hint that it "will be an error in Nimbus 1.0". Protects against silent typos (tokne = "…"instead oftoken) and forward-compatible downgrade changes that were previously ignored without feedback. Opt-in fail-mode via[controller] strict_config = true— unknown keys become aConfigExceptionstartup abort instead of just a warning. Default in 0.13 isfalse(warn-only); 1.0 flips the default totrue. User-editable message template files (AuthMessages,PunishmentsMessages) remain intentionally lenient so new placeholder keys in future versions don't retroactively warn on existing files. - Cluster protocol version handshake —
ClusterMessage.AuthRequestandAuthResponsenow carry an explicitprotocolVersionfield (currentlyCURRENT_PROTOCOL_VERSION = 1). During the handshake the controller compares the agent-reported version with its own before checking the token. A mismatch producesAuthResponse(accepted = false, reason = "protocol version mismatch: agent=X, controller=Y")and a WS close withVIOLATED_POLICY. On the agent side,AgentRuntimedetects the fatal reject and immediately breaks the reconnect loop (running = false, no exponential backoff), preventing the previous reconnect churn on incompatible deployments — instead of looping silently for hours, the agent logs a clear ERROR once and exits. Field defaults (= 1) plusignoreUnknownKeys = trueinclusterJsonpreserve the rolling-upgrade path between 0.12.x and 0.13.0 nodes. Bump rule: only increment when an existing field is semantically changed, removed, or renamed — new@SerialNamemessage types don't need a bump.
Changed
- Module API package renamed — all public module API types (
NimbusModule,ModuleContext,ModuleCommand,AuthPrincipal,PermissionSet,PluginDeployment,PluginTarget,Migration,DoctorCheck,DoctorLevel,DoctorFinding,CommandCaller,CommandOutput,SubcommandMeta,CompletionMeta,CompletionType,AuthLevel,DashboardConfig,DashboardSection,SessionValidator, and the extensionshasPermissionandservice<T>()) have been moved fromdev.nimbuspowered.nimbus.moduletodev.nimbuspowered.nimbus.module.api. Goal: cleaner separation between the public module API and core-internal types (ModuleManager,ModuleContextImpl,ModuleInfo). ApiErrorenum consolidation — BREAKING — the looseApiErrorsstring constants are now consolidated into a typedenum class ApiError(val code: String, val defaultStatus: HttpStatusCode)that all routes use uniformly for error codes and default HTTP status.object ApiErrorsis retained as a@Deprecatedcompat facade delegating to the enum internally — wire strings (VALIDATION_FAILED,SERVICE_NOT_FOUND, …) are byte-identical. Restructured into families:AUTH_*(TOTP, Session, Magic-Link, Login-Challenge) andBACKUP_*for all backup module errors, plus existing core codes. Removed the unusedINSUFFICIENT_SCOPEcode. No changes needed on the API consumer side.- Permission node rename — BREAKING —
nimbus.playersis nownimbus.cloud.players, consistent with the rest of thenimbus.cloud.*admin family. Auto-migrated on startup:PermsModule.enable()calls the newPermissionManager.renamePermissionInAllGroups(old, new)API, which works idempotently across all groups — global, negated, context-scoped. Operators who need to defer the migration can set[permissions] skip_node_migrations = trueinnimbus.toml. A new complete permission registry indocs/reference/permissions.mdxdocuments all three families:nimbus.cloud.*(admin/operations),nimbus.dashboard.*(web UI), andnimbus.<module>.<behavior>(in-game gameplay). - SDK public surface frozen — the stable API surface of
plugins/sdkis now documented and guarded against accidental changes. Classes without@ApiStatus.Internalform the supported API contract — breaking changes only in major versions with a documented migration guide;@ApiStatus.Internal-marked classes are implementation details and may change between minor versions without notice. Newpackage-info.javafiles insdk/,sdk/compat/, andsdk/event/document the contract at the package boundary. A newSdkPublicSurfaceTestregression test locks both the stable and internal lists against unintended reclassification via class-file scanning.
Docs
- API stability policy — new reference page
docs/reference/api-stability.mdxdocuments the SemVer guarantees for the/api/*URL space (additive minor changes, removals/breaks in major versions only), theDeprecation:header path including sunset window, the status of error codes (ApiErrorenum) as part of the wire contract, and the boundary with the independently versioned cluster protocol (protocolVersion). Also explains why Nimbus deliberately does not introduce a/api/v1/URL prefix (private admin API, consumers are deployed together with the controller). NimbusEvent.actorgrammar — theactor: Stringfield onNimbusEventnow has explicit KDoc grammar: either a bare type or<type>:<id>, withtype ∈ { "system", "console", "api", "player", "agent" }. New extensionNimbusEvent.actorType()returns the type prefix for filtering and auditing, formalising what was previously a convention in audit-log and event-stream entries.
Migration
- Third-party modules: Existing imports continue to work via deprecated type aliases but will produce compiler warnings. Migrate all imports of the form
dev.nimbuspowered.nimbus.module.<Type>todev.nimbuspowered.nimbus.module.api.<Type>. The compatibility shim is removed in 0.14.0. - Permission node
nimbus.players→nimbus.cloud.players: No operator action required — auto-migration runs on first start of 0.13.0, adding the new node to any group that already has the old one (preserving negations and contexts). The old node remains functional until 0.14.0; group exports and external tooling scripts should be updated before upgrading.
[0.12.0] - 2026-04-20
Minor release focused on v1.0-readiness hardening: kernel-enforced resource caps for bare-process services, operator-readable crash diagnostics, richer Prometheus metrics, a structured /api/reload response, opt-in JSON logging for Loki/Elastic, and a long-standing silent-fallback bug in the software resolver. All additions are additive and opt-in — existing nimbus.toml / groups/*.toml files keep working unchanged.
Added
- Managed sandbox (
[sandbox]/[group.sandbox]) — services can now run inside their own cgroup v2 scope viasystemd-run --user --scope, with kernel-enforcedMemoryMax,CPUQuota, andTasksMax. Defaultdefault_mode = "auto": on Linux hosts with a reachable user systemd bus Nimbus silently wraps every service spawn; on macOS, WSL1, or hosts without user systemd it transparently falls back to bare (ProcessBuilder) with a single INFO log at startup. Per-group override via[group.sandbox] mode = "bare"|"managed"|"docker"and optionalmemory_limit_mb/cpu_quota/tasks_max. Memory caps auto-derive from[group.resources] memory+ a 30 %/256 MB JVM overhead budget. Fixes the audit finding that bare JVMs have no hard native-memory cap and can OOM-kill neighbours on the same host - Startup crash diagnosis + stdout tail — when a service transitions
STARTING → CRASHED(ready-timeout or early exit before READY), Nimbus now attaches a one-line operator-readable diagnosis and the last ~50 stdout lines to both theServiceCrashedevent and the newService.lastCrashReportfield. Pattern classifier covers the realistic majority of failures: bound-port conflicts (with port extraction), JVM OOM (OutOfMemoryError), kernel/cgroup OOM-kill (exit 137), missing JAR, Java version mismatch (UnsupportedClassVersionError), stalesession.lock, EULA, ready-pattern timeout. Messages are English to match the rest of the codebase. Dashboards and REST clients can render the report without replaying the full stdout stream - Prometheus metrics — five new series on the existing
/api/metricsendpoint:nimbus_warmpool_size{group},nimbus_warmpool_target{group},nimbus_service_crashes_total{group},nimbus_scaling_events_total{group,direction="up|down"},nimbus_placement_blocked_total{group}. Counters reset on controller restart by design — Prometheusrate()/increase()handles resets natively, and persisting these would just duplicate whatMetricsCollectoralready keeps in the DB for long-running dashboard charts - Structured reload report —
POST /api/reloadand the consolereloadcommand now return/display aReloadReportnaming every known config section with itsReloadScope(LIVE/NEXT_SERVICE_PREPARE/REQUIRES_RESTART), whether the current pass applied it, and which sections need a full controller restart to take effect. Backwards-compatible — the legacysuccess,groupsLoaded, andmessagefields are retained, so older dashboard versions keep working unchanged. 22 sections are enumerated out of the box (groups, dedicated, syncproxy/motd/tablist/chat, backup, audit, api, database, cluster, network, sandbox, java, loadbalancer, controller, bedrock, punishments, resourcepacks, dashboard, curseforge, console, metrics). Ends the "I editednimbus.tomland nothing changed" confusion that the previous one-line hint at the end ofreloadoutput always missed - Opt-in JSON logging — set
NIMBUS_LOG_FORMAT=jsonin the environment before starting the controller to switch from the default plain-textlatest.logpattern to one-object-per-line JSON vialogstash-logback-encoder. Designed for ingestion into Loki, Elasticsearch, or Splunk. MDC keys pre-whitelisted:service_name,group_name,node_id,request_id,api_user— so any futureMDC.put(...)site starts emitting rich fields without further config changes. Default plain-text mode is unchanged - Unknown server-version hard-fail + NeoForwarding bootstrap —
SoftwareResolvernow throwsUnknownServerVersionExceptioninstead of silently falling back to an arbitrary version when a group requests a Minecraft version that upstream APIs don't know about (Paper/Purpur/Velocity/NeoForge/Fabric).CreateGroupCommandsurfaces the list of known versions on this failure so operators get an actionable error instead of a mystery backend. Service startup additionally detects whether the NeoForwarding mod is needed based on the loaded MC version (≥ 1.20.2) and the configured forwarding mode
Changed
- Docs —
[sandbox]and[group.sandbox]added to thenimbus.tomlreference and the group-config guide. The/api/reloadresponse example updated to show the newReloadReportshape with legacy-field compatibility, and the/api/metricssection lists the five new counter/gauge series
Fixed
CompatibilityCheckerno longer crashes on groups with unresolvable software versions — the upstreamUnknownServerVersionExceptionis caught and turned into a per-group compatibility warning rather than aborting the preflight. Groups that can be resolved still preflight normally
Security
- Release notes carry no security fixes. The audit follow-ups from v0.11.1 (brute-force throttle, auth events, HTTPS GeoIP) remain the current state
Known follow-ups (deferred)
- Agent-node sandbox — managed mode currently wraps only controller-local spawns. Cross-node enforcement requires a
ClusterMessage.StartServiceschema addition and is tracked for a follow-up - Windows Job Objects — a JNA-based Windows backend for the managed sandbox is sketched but not shipped in this release; Linux covers the overwhelming majority of Nimbus deployments
ModuleContext.registerReloadableSection— module-api extension is deferred; the Backup module keeps its ownPUT /api/backups/configendpoint for hot-reload- MDC emit sites — the JSON config already whitelists the keys; future
MDC.putcalls inServiceManagerand API routes will start emitting rich fields without further config churn
Notes
- Linux operators who want to disable the default managed wrapping can either set
[sandbox] default_mode = "bare"globally or[group.sandbox] mode = "bare"per group. A quick verification after upgrade: start any service and checksystemctl --user list-units | grep nimbus-— you should see a*.scopeunit per running service nimbus_service_crashes_totalinfers the group label from the<Group>-<N>service naming convention; dedicated services (which have no numeric suffix) are tallied under their own name- Existing tests extended by 39 new cases across the five feature areas; full
nimbus-coresuite stays green
[0.11.2] - 2026-04-19
Patch release focused on internal quality: a TOTP recovery-code bug that silently broke 2FA recovery, a comprehensive test suite + JaCoCo coverage scaffolding across every Gradle subproject, and small dashboard polish for the OTP / login flow. Fully backwards-compatible; no migration changes.
Added
- Test suite + JaCoCo coverage — JUnit 5 + MockK/Mockito wired up across all 22 Gradle subprojects (everything except the dashboard, which is a Next.js app), 880+ tests green. Per-subproject JaCoCo reports plus an aggregated root report (
./gradlew jacocoAggregatedReport) filtered todev/nimbuspowered/**. Baseline coverage: line 12.5 %, branch 7.7 %, method 14.9 %, class 15.6 %. Top-covered modules:nimbus-protocol88 %,display40 %,backup33 %,auth29 %,resourcepacks29 %,punishments28 %,players28 %
Changed
- Dev tooling — Java plugin subprojects now scope
-source 16 -target 16tocompileJavaonly, socompileTestJavacan use Java 21 features. A handful of TOTP / scaling helpers were promoted fromprivatetointernalso tests can hit them without reflection. No public API change - Dashboard
next.config— relaxed dev-server allowed origins and lifted image-quality limits, smoothing local dashboard development against a remote controller. Production build behaviour unchanged - Repo housekeeping — removed the unused
examples/directory, gitignored/.gradle-impl-plugins/
Fixed
- TOTP recovery codes were never accepted.
TotpService.enrollhashed the display form (ABCD-1234, with the dash) whileconsumeRecoveryCodenormalized the dash away before hashing, so every recovery code looked invalid in production. Both paths now hash the canonical (dash-stripped, uppercased) form via a sharedcanonicalizeRecoveryCodehelper. Anyone who has already lost their TOTP device and saved recovery codes from a v0.11.0 / 0.11.1 enrollment will need to re-enroll — old hashes are unreachable. Regression tests added - Dashboard login form / OTP input — tightened the 6-digit code + recovery-code inputs (focus handling, paste behaviour, layout shift on the login card). Cosmetic only, no auth-protocol change
Notes
- 15 pre-existing
nimbus-coretest failures (drifted from a permissiveServiceStatetransition model, the bounded-bufferEventBus, the player-count gate on the scale-down loop, and the re-introduced/api/shutdown) were repaired during the coverage push. No production behaviour was changed to make tests pass
[0.11.1] - 2026-04-19
Quick-win patch from the v0.11 production-readiness audit. Tightens the new auth surface and adds proper audit-trail coverage for dashboard logins. Fully backwards-compatible; no migration changes.
Added
- Per-IP brute-force throttle on
consume-challenge. New[login_challenge] max_consume_failures_per_minute = 10inauth.toml. Defends the 6-digit code's 60s TTL window — the global API limiter (120 req/min) was the only previous defence, which was too coarse for a 10⁶ search space. Set to0to disable - Auth events on the
EventBus—DashboardLoginSucceeded,DashboardLoginFailed,DashboardSessionRevokedare now emitted on every login, failed login, and session revocation. TheAuditCollectorpersists them toaudit_logautomatically — visible via theauditconsole command andGET /api/audit. Each event carries the source IP, affected UUID (when known), and a reason/scope tag [console] geo_lookup_enabled = falseinnimbus.toml— kill-switch for the Remote-CLI session GeoIP lookup. Default flipped tofalsein v0.11.1; flip on if you want city-level location strings in your CLI session log. Privacy-preserving by default
Changed
- GeoIP lookup uses HTTPS instead of cleartext HTTP. The previous
http://ip-api.com/json/...call leaked the connecting operator IP to passive observers on the network path. The free tier of ip-api.com may now return errors; combine withgeo_lookup_enabled = falseto disable entirely - Docs —
[Auth Module]page now documents the API-token bypass behaviour explicitly: holdingNIMBUS_API_TOKENis implicitnimbus.dashboard.adminand bypasses TOTP. Treat the token like a root SSH key. (Behaviour unchanged from v0.11.0, but undocumented before.)
Known follow-ups (deferred)
- 6 pre-existing dashboard ESLint errors (setState-in-effect / refs-during-render) blocking Vercel deployment — separate dashboard-only PR.
- Module-level integration test harness — tracked for v1.0 readiness.
[0.11.0] - 2026-04-19
A new first-party Auth module: the dashboard gains real multi-user authentication backed by Minecraft accounts, with no passwords, no OAuth provider, and no per-user credential files. Operators log in by typing a 6-digit code in-game, clicking a chat link, or tapping Touch ID — sessions are then gated by a nimbus.dashboard.* permission tree resolved through the existing Perms module. API tokens keep working as implicit admins so every existing CLI, automation script, and CI pipeline survives untouched.
Added
- Auth module — Dashboard authentication + RBAC, backed by the Perms module for permission resolution. Loaded as a controller module (migration range 8000+), auto-deploys its own Velocity plugin, and exposes four login paths that all issue the same shape of session.
- MC-account code login. The user types a name in the dashboard, Nimbus mints a 6-digit challenge and sends it to the online player via
/nimbus dashboard login. The in-game command lives on the Bridge-forwardedModuleCommandpathway that Punishments/Perms/Scaling already use — no separate/dashboardcommand, no custom Velocity plumbing on the module side. Caller UUID is forwarded so the command is scoped to the player typing it - Magic-link login. Alternative to the code path: dashboard calls
POST /api/auth/deliver-magic-link, controller fires the newAUTH_MAGIC_LINK_DELIVERYmodule event, the auth-velocity plugin subscribes and pushes a clickable chat component to the target player. One-time-use, TTL-expiring, URL contains only a random token (no PII). Rate-limited on the public delivery endpoint - Passkey / WebAuthn login. Built on
com.yubico:webauthn-server-core:2.6.0(shaded into the core fat JAR so per-module classloader isolation still holds). MC UUID packs into a 16-byteuserHandleso browsers can offer discoverable credentials — no need to type a username first. After a one-time enrollment via an existing session, users authenticate with Touch ID / Windows Hello / a platform or roaming security key. Ceremony state is held in a TTL-bounded in-memory cache; registered credentials live in the newdashboard_webauthn_credentialstable with the COSE public key, a monotonic sign counter for cloning detection, the MC name at enrollment time, and an operator-supplied label per device - TOTP 2FA + recovery codes. RFC 6238 HMAC-SHA1 6-digit codes with a configurable
±Nstep window. Secrets stored AES-GCM-encrypted withconfig/modules/auth/session.key(auto-generated, chmod 600, never rotate without invalidating every 2FA enrollment). Ten single-use recovery codes issued at setup, SHA-256 hashed at rest. Half-authchallengeIdissued in-memory during TOTP verification so the 2FA step is crash-ephemeral by design - 7-day rolling sessions. Session tokens never land on disk as raw strings — the table stores only a SHA-256 hash of the token; the raw value lives exclusively in the dashboard's httpOnly cookie /
localStorage. Sessions carry a permission snapshot at issuance time; the Perms module firesPermissionsChangedevents and the Auth module refreshes affected snapshots on the fly instead of re-reading on every request - API tokens keep working as implicit admins. Every existing bearer-token call gets
nimbus.dashboard.*access automatically. Zero breaking changes for CLI tooling, automation, or the Remote CLI
- MC-account code login. The user types a name in the dashboard, Nimbus mints a 6-digit challenge and sends it to the online player via
- Permission model —
nimbus.dashboard.*nodes resolved through the Perms module. Route guards live on every admin-facing endpoint (services, groups, dedicated, config, files, modules, system, audit, cluster, players, punishments-per-action, resourcepacks, scaling, maintenance). Public endpoints (/api/health), SDK-telemetry endpoints, and cluster-token endpoints (/api/cluster/*,/api/services/.../state/*) are intentionally unguarded. Permission checks for the API-token principal short-circuit totrueso nothing breaks for operators who never enable user logins - Velocity plugin —
nimbus-auth-velocity.jar. Auto-deployed by the Auth module to every proxy on every service prepare. Does two things:- Subscribes to
AUTH_MAGIC_LINK_DELIVERYmodule events and pushes clickable chat components viaTextCompat.sendClickableLink(works on Adventure-native and legacy BungeeCord-chat proxies alike) - Does not register its own
/dashboardcommand — login/sessions/logout-all live under/nimbus dashboard ...via the sharedModuleCommand→ Bridge/nimbuspattern, consistent with every other module's in-game surface
- Subscribes to
- Message templates at
config/modules/auth/messages.toml— operator-customisable code/link/session strings with{code},{url},{click},{ttl}placeholders so networks can stamp their own branding on the login prompt - Self-service session management. New user-scoped endpoints (
GET /api/auth/my-sessions,DELETE /api/auth/my-sessions/{id},POST /api/auth/my-sessions/revoke-others) let signed-in users see where else they're logged in and revoke individual sessions or "sign out everywhere else" without admin intervention. The existingGET /api/auth/sessions+POST /api/auth/logout-allendpoints are now SERVICE-auth only, for the in-game command path
Core changes (minimal, non-breaking)
All additive — installations that don't enable the Auth module keep working in token-only mode.
ModuleContext.getServiceByClassName(fqcn: String)— name-based cross-classloader service lookup. Every module has its ownURLClassLoader, so holding a compile-timeClass<?>reference to a service registered by another module doesn't work (class identity is classloader-scoped). ExistinggetService<T>()keeps working for same-classloader lookups; the new method walks registered service classes + their supertypes to find a match by name. Needed so the Auth module'sPermissionResolvercan reach the Perms module'sPermissionManagerreflectivelyModuleCommand.execute(args, output, caller: CommandCaller?)overload. Default implementation delegates to the caller-less variant, so every existing module command keeps compiling unchanged.CommandCaller(uuid, name)lives inmodules/apiso modules can declare caller-scoped subcommands without depending on VelocityCommandExecuteRequestcarries an optionalcallerDTO end-to-end (Bridge → controller).CloudCommand.executeRemoteCommandpopulates it when the source is a VelocityPlayer[dashboard] public_urlfield innimbus.toml— canonical base URL the dashboard is reachable at. Used for magic-link construction and as the default RP origin for WebAuthnNimbusModule.requireshonoured during module load — the Auth module declaresrequires = ["perms"]. If Perms isn't installed the auth module falls back to token-only mode and logs aWARN; it never silently does nothing
Refactors
Neither ships any behavior change on its own, but both needed to happen before v0.11 could feel clean:
-
Module-owned plugins co-locate with their modules.
plugins/now only holds core plugins deployed on every service/proxy (plugins/sdk,plugins/bridge). Module-specific Minecraft-side code moves next to its owning module so each module + its plugin form one logical unit on disk:plugins/perms→modules/perms/pluginplugins/display→modules/display/pluginplugins/resourcepacks→modules/resourcepacks/pluginplugins/punishments-backend→modules/punishments/plugin-backendplugins/punishments-velocity→modules/punishments/plugin-velocityplugins/auth-velocity→modules/auth/plugin-velocity(new in this release)
Pure disk-layout refactor: Gradle project names (
:nimbus-perms,:nimbus-auth-velocity, ...) are unchanged — onlyprojectDirmappings insettings.gradle.ktswere updated.PluginDeploymentresourcePaths (plugins/nimbus-*.jar) stay the same because they reference the shadow-jar's internal resource layout, not source paths -
TextCompat.sendClickableLink+AdventureHelper.sendClickableLinkadded to the SDK as general-purpose cross-version helpers. Kept in the SDK rather than the Auth module because they're genuinely reusable — any module that wants to send clickable chat components from a Velocity plugin now has a one-liner
Database
Four new tables under the 8000+ migration range:
dashboard_sessions— token hash, uuid, mc name, login method, issued/last-used timestamps, cached permission snapshotdashboard_login_challenges— pending 6-digit codes + magic-link tokens, TTL-expireddashboard_totp— AES-GCM-encrypted secrets + enrollment statedashboard_recovery_codes— SHA-256 hashed, single-usedashboard_webauthn_credentials— COSE public key, sign counter, AAGUID, label, mc name at enrollment
Configuration
- New
config/modules/auth/auth.toml—[sessions],[totp],[webauthn]blocks. RP ID and origins default to values derived from[dashboard] public_url, configurable for localhost / reverse-proxy setups - New
config/modules/auth/messages.toml— operator-customisable in-game strings - New
config/modules/auth/session.key— auto-generated 32-byte AES-GCM key, chmod 600. Back this up — losing it invalidates every TOTP enrollment (recovery codes still work, passkeys still work, sessions still work) - New
[dashboard] public_urlinnimbus.toml— required if you want magic-link URLs to point at the right host
API
POST /api/auth/generate-code(SERVICE),POST /api/auth/consume-challenge(public),POST /api/auth/request-magic-link(SERVICE),POST /api/auth/deliver-magic-link(public, rate-limited),POST /api/auth/consume-magic-link(public)GET /api/auth/me,POST /api/auth/logoutPOST /api/auth/totp-verify(public; accepts live TOTP or recovery codes)GET /api/profile/totp/status,POST /api/profile/totp/enroll|confirm|disablePOST /api/auth/passkey/{register,login}/{start,finish};GET /api/auth/passkey/credentials,DELETE /api/auth/passkey/credentials/{id}GET /api/auth/my-sessions,DELETE /api/auth/my-sessions/{sessionId},POST /api/auth/my-sessions/revoke-othersGET /api/auth/sessions(SERVICE),POST /api/auth/logout-all(SERVICE)
Docs
- New guide:
/docs/guide/dashboard-auth— end-user login flow, 2FA, passkeys, permission model, recommended group templates, backwards-compat matrix, security notes - New reference:
/docs/config/auth—auth.tomlfield-by-field, endpoint table,session.keybackup warning
Security hardening (pre-release review)
Fixes landed on top of the phase-1 through phase-9 feature work, driven by a pre-release review pass:
- TOTP replay prevention (RFC 6238 §5.2). New
last_used_stepcolumn (migrationV8005) ondashboard_totp;verifyAndAdvanceatomically advances the step counter so an observed code cannot be re-accepted within the same or any earlier step inside the tolerance window - TOTP window cap tightened. Config
[totp] windownow coerces into0..2(was0..10). Default1= ±30 s, hard max2= ±60 s; paired with the replay column the effective "how long is a captured code live" dropped from potentially 5 minutes to a single 30-second step - WebAuthn
CredentialRepositoryis pre-fetch only. The earlier implementation calledrunBlocking { newSuspendedTransaction { … } }from inside Yubico's synchronous callbacks, which could deadlock the Ktor IO pool under load. Each ceremony now snapshots the relevant rows into an in-memoryCredentialCachebefore entering the library, and the in-memory repo serves lookups without doing any I/O allowOriginPortis operator-gated. New[webauthn] allow_origin_portconfig key, off by default. Earlier iteration hard-codedtruewhich weakened phishing-resistance (host:443andhost:3000were treated as the same origin). Flip on only for local dev; production keeps strict origin binding- Short-lived WebSocket tickets. New
GET /api/auth/ws-ticket(bearer session) mints a 30-second, single-usewt_token. The dashboard exchanges its bearer for a ticket before opening any WS or SSE connection, so the long-lived session token never lands in a URL query string, access log, or referrer header.SessionValidatortransparently redeems tickets on first use - Rate-limit state now DB-backed. The per-UUID 5-per-minute quota on code + magic-link issuance is counted from
dashboard_login_challengesrows instead of an in-memory map — a controller restart no longer resets the counter PLAYER_OFFLINEno longer leaks existence. The dashboard-initiated magic-link delivery endpoint now returns a generic "Player not found — please join any Nimbus server and try again" for every negative outcome (offline, never existed, Players module absent)- Permission snapshots stored as JSON.
permissions_snapshotis now a JSON array, so nodes containing commas round-trip cleanly. Legacy comma-joined snapshots are still readable — existing sessions survive the upgrade - Session token persistence moved to
sessionStorage. Session bearers now survive a page refresh without moving tolocalStorage(no cross-tab reads, cleared on tab close). API tokens continue to live in-memory only - Passkey login is treated as MFA-equivalent. A WebAuthn assertion proves possession (authenticator holds the private key) + user verification (biometric or PIN), so successful passkey login doesn't additionally prompt for TOTP. Stricter requirements should be enforced via the authenticator's user-verification policy rather than stacking redundant factors. Code + magic-link logins continue to gate on TOTP
Notes
- No breaking changes for existing token-based setups. Bearer tokens keep working identically. Upgrading from 0.10.0 is drop-in — the auth module migrations run on first boot, no operator action required
- Phase-1 scope for audit logging. Auth events (login, logout, TOTP enroll/disable, passkey enroll/delete) land in the existing
audit_logtable via the standardAuditCollector. A dedicated Auth-module audit view is a follow-up - Integration tests deferred. The existing module test harness doesn't cover full Ktor + Perms integration; adding it properly is its own work item. The login flows have been exercised end-to-end on a local install against a real MC account with both platform and roaming authenticators
[0.10.0] - 2026-04-17
A new first-party Docker module: services can opt in to running inside Docker containers instead of as bare Java processes, with hard kernel-enforced resource limits, clean cleanup, and mixed-Java-version support — all while the process path stays the untouched default for the rest of the installation.
Added
-
Docker module — Run services as Docker containers, opt-in per group / dedicated via
[docker] enabled = true- Bare HTTP/1.1 client over Unix Domain Sockets. The module talks to the Docker Engine API via JVM 16+
SocketChannel.open(StandardProtocolFamily.UNIX)— no new dependencies, nodockerCLI requirement, and TCP fallback for Docker Desktop / rootless setups (tcp://localhost:2375). Pinned to API v1.41 (Docker 20.10 / ~2021) for broad compatibility; verified against 29.4 + API 1.54 in testing - TTY-mode attach for bidirectional I/O. Containers are created with
Tty: true+OpenStdin: true+AttachStdin: trueso Nimbus owns a single hijacked stdio stream — stdout lines flow into the existingSharedFlow<String>, andsendCommand("stop")writes straight to the Minecraft server's stdin. No RCON, no extra ports - Resource limits enforced by the kernel.
memory_limitmaps to cgroupMemory,cpu_limittoNanoCpus(fractional cores supported).MemorySwapis pinned toMemoryto disable swap thrash. Restart policy is fixed tono— Nimbus handles restart decisions, not Docker - Auto image selection. Picks
eclipse-temurin:17-jre/eclipse-temurin:21-jrebased on the server's required Java version; falls back to a configurable default. Per-group override via[group.docker] java_image = "…". First-use pull is automatic via/images/create - Auto network.
nimbusbridge network is created on first start; every container attaches to it so they can reach each other by container name. Port mapping is same-port (host:N → container:N) soserver.properties/velocity.tomlkeep their existing port without rewriting - Bind-mount, not custom images. The entire service work dir is bind-mounted at
/server— templates, plugins, JARs, and worlds come from the host filesystem. No Dockerfile authoring, no image build pipeline; Phase 1 ships with this plus a deferred door for pre-built images later - Container crash recovery. On controller restart, the module enumerates every
nimbus.managed=truecontainer and reattaches to the running ones — no service restart. The attach stream picks up fresh stdout and the exit watcher resumes exactly as if the handle had just been created.ServiceManager.recoverLocalServices()prefers Docker-reattached handles overProcessHandle.adopt()so the post-restart PID (stale across container rebuilds) doesn't confuse anything - Docker stats as the memory source. When a service is container-backed,
ServiceMemoryResolverreads cgroup memory fromdocker statsinstead of/proc/<pid>/status. Catches sidecar processes the plain JVM PID would miss; dashboard numbers matchdocker statsexactly - Fallback on daemon down. If the daemon isn't reachable at service-start time, the module logs a warning and the service transparently falls back to a bare process — the rest of the installation keeps working. Doctor flags the daemon state so the operator notices
- Console command
docker <status|ps|inspect|prune>with tab completion. Supports inspecting live stats for any container by name or short ID - REST API (ADMIN-only):
GET /api/docker/status,GET /api/docker/containers,GET /api/docker/containers/{name}(with live cgroup stats),POST /api/docker/prune - Doctor check: reports daemon reachability, API version (warns on pre-1.41), and Nimbus container counts; surfaces in both the
doctorconsole command and/api/doctor
- Bare HTTP/1.1 client over Unix Domain Sockets. The module talks to the Docker Engine API via JVM 16+
-
Dashboard — Docker page at
/modules/docker- Four status cards: module enabled, daemon reachable, daemon version + API version, total/running container counters
- Daemon-offline banner with the configured socket path when reachable=false, so the operator sees why opted-in services are running as plain processes
- Nimbus-managed container table with state badges (running / created / paused / exited mapped to the
--severity-*tokens), image, ports, short ID, and a prune action that callsPOST /api/docker/prune - Polls via
POLL.normalwith visibility pausing; the list is silent on transient failures so a brief daemon hiccup doesn't spam toasts
-
Dashboard — per-group Docker toggle.
GroupEditDialoggrew a Docker section: enable switch + memory limit + CPU limit + Java image. Empty fields mean "inherit module default"; when the switch is off, no overrides are written and the TOML stays clean -
Dashboard — Docker badge. The services list renders a sky-blue
Dockerbadge next to each containerised service. Backed by a newbackedBy: "docker"field onServiceResponse
Core changes (minimal, non-breaking)
All additive — nothing breaks for existing installations that don't enable the module.
DockerServiceConfigonGroupDefinitionandDedicatedDefinition(defaults toDockerServiceConfig()withenabled = false, so services without a[docker]block behave exactly as before)PreparedService.dockerConfigpopulated byServiceFactoryfrom the group/dedicated configLocalServiceHandleFactoryinterface innimbus-core/service/— modules register an alternativeServiceHandlefactory viaModuleContext.registerService().ServiceManager.startLocalServiceconsults it per-start and falls back toProcessHandlewhen the factory is absent orisAvailable()returns false.recover()hook on the same interface lets the module reattach to running containers on restartServiceMemorySourceinterface +ServiceMemoryResolver.registerSource()— pluggable memory reader so the Docker module can serve cgroup stats ahead of/procServiceHandle.kind: Stringwith default"process"— tagging mechanism so/api/servicescan surfacebackedBywithout the core having to know about container implementationsGroupResponse/CreateGroupRequestcarry an optionaldockersub-object.buildGroupTomlconditionally emits[group.docker]only when something's set, so existing TOML files round-trip unchanged
Notes
- Phase 1 scope. The module runs on the controller only — agent-node Docker support, pre-built custom images, Windows named-pipe sockets (
\\.\pipe\docker_engine), and image-pull progress streaming are all tracked for follow-up phases. TCP viatcp://localhost:2375covers the Windows gap today - Podman is Docker-API-compatible and should work best-effort against the module pointed at a Podman socket, but it's not part of the CI test matrix
- Password-less daemon access. The Nimbus user must be in the
dockergroup or the daemon socket must allow the UID. No sudo at runtime
[0.9.1] - 2026-04-15
A new first-party Backup module: scheduled tar+zstd snapshots of services, templates, controller config, the state-sync store, and the database, with GFS retention, single-pass SHA-256 manifests, and a full editor on the dashboard.
Added
-
Backup module — Scheduled snapshots of all stateful Nimbus data to local
.tar.zstarchives- Native multi-threaded archiver. In-JVM pipeline via Apache Commons Compress (
TarArchiveOutputStream) + zstd-jni (ZstdOutputStreamwithsetWorkers(N),setCloseFrameOnFlush(false)) running at 3–5× the throughput of piping totar --zstd. The speedup comes from three places: libzstd's parallel compressor (coreutils tar pipes into the single-threaded binary), no fork/exec + stdout pipe stage, and a 256 KiB upstream buffer that keeps the compressor saturated on worlds with thousands of tiny region files - Single-pass SHA-256. Each file's hash is computed while the bytes stream through the archiver, written as a trailing
MANIFEST.sha256entry, and re-checked bybackup verify <id>— one filesystem read per file, not two - Six scope types: services, dedicated, templates, controller config, state-sync canonical store, database. Each produces one archive per target, independently verifiable and restorable
- Database dumps. SQLite via
VACUUM INTOinside an Exposed transaction — atomic, no external tool. MySQL viamysqldump --single-transaction --routines --triggers --events, Postgres viapg_dump --format=custom— skipped with aWARNand aPARTIALrow if the tool is missing onPATH - Cron scheduler with a hand-rolled 5-field POSIX evaluator (
*,N,N-M,N,M,O,*/5,0-30/5; Sunday = 0 or 7). Default config ships hourly + daily + weekly schedules - GFS retention. Per
(targetType, targetName, scheduleClass), Nimbus keeps the N most recent SUCCESS/PARTIAL rows. FAILED rows don't count against the budget — a transient failure doesn't cost you a retained snapshot.retention.keep_manual = true(default) makes manual backups immune to automatic pruning - Quiesce via the existing
ServiceManager.executeCommandplumbing —save-off+save-all flush, configurable wait,save-onafter archiving. Only applies to services inREADY/STARTINGstate on the local node; remote-node services are skipped withPARTIALstatus until cluster streaming lands in a later phase - Restore with path-traversal protection — extracts to a staging dir, atomic-renames into the target. Refuses to overwrite a running service unless
--forceis passed - Console command
backup <now|list|status|restore|verify|prune|schedule>with tab completion - REST API (ADMIN-only):
GET /api/backups,GET /api/backups/{id},POST /api/backups/trigger,DELETE /api/backups/{id},POST /api/backups/{id}/restore,POST /api/backups/{id}/verify,GET /api/backups/{id}/download(streaming),GET /api/backups/{id}/manifest,GET /api/backups/schedules,GET /api/backups/status,POST /api/backups/prune,GET /api/backups/config,PUT /api/backups/config - Live config editing.
PUT /api/backups/configvalidates (cron syntax, compression level 1..22, unique schedule names, allowed target keys, known retention classes) and atomically rewritesconfig/modules/backup/backup.tomlwith a deterministic layout + comments. Scheduler is hot-reloaded on success;400 VALIDATION_FAILEDleaves the in-memory state untouched - Retention loop runs automatically every hour in addition to on-demand
backup prune - Migrations:
BackupV1_Baseline(v7000 —backups+backup_schedule_logtables) - Events:
BACKUP_STARTED,BACKUP_COMPLETED(with duration + size),BACKUP_FAILED,BACKUP_RESTORED,BACKUP_PRUNED
- Native multi-threaded archiver. In-JVM pipeline via Apache Commons Compress (
-
Dashboard — Backup page at
/modules/backupwith two tabs:- Overview: four stat cards (total / storage / schedules / last run), a schedules table showing last-run status + next-fire time, and a backup history table with inline Verify / Download / Restore / Delete actions. Download goes through an authed blob fetch + client-side
<a download>so the ADMIN token isn't exposed in URLs. Empty states use call-to-action buttons that jump to the Settings tab or open the "Run backup" dialog, not file paths - Settings: full editor for every knob in
backup.toml. Schedule add/edit dialog with cron field, retention-class pills, and target-selection pills. Retention budgets, scope toggles with hints, compression level + workers (with a note on the 3–5× speedup), quiesce toggle + wait seconds, exclude patterns textarea. Save validates on the server and hot-reloads; errors surface in the page banner with the exact offending field - Polling is visibility-aware and adaptive — 30 s idle, 5 s while a job is running, paused when the tab is hidden. No busy-loop on the global rate limit
- Overview: four stat cards (total / storage / schedules / last run), a schedules table showing last-run status + next-fire time, and a backup history table with inline Verify / Download / Restore / Delete actions. Download goes through an authed blob fetch + client-side
Changed
- Sidebar breadcrumbs.
backup,punishments,resourcepacks,doctor,stats, andloginnow have proper capitalized labels instead of falling through to the raw URL segment - Module discovery icons. The sidebar's
iconMapgainedArchiveandHardDrivemappings so modules with thoseDashboardConfig.iconvalues render correctly - Dependencies.
nimbus-corenow shadescom.github.luben:zstd-jni:1.5.6-4andorg.apache.commons:commons-compress:1.27.1(~1.8 MB) so modules can rely on them without wiring their own copies
Notes
- Phase 1 scope. Local destination only; remote agent-node services are skipped with
PARTIALstatus. S3/SFTP destinations, cross-snapshot dedup, and cluster streaming are tracked for follow-up work via aBackupDestinationinterface and a newBackupStreamRequestcluster message, respectively - No encryption in v1. Archives contain world data and DB dumps — use filesystem-level or destination-level encryption if needed, and don't expose
data/backups/via nginx
[0.9.0] - 2026-04-14
Two new first-party modules — Punishments and Resource Packs — plus a refactor of how Nimbus deploys its own plugins. Templates are now fully user-owned: the SDK, Bridge, and every module's plugin are deployed on each service prepare and never written into templates/global/ or templates/global_proxy/.
Added
-
Punishments module — Network-wide BAN / TEMPBAN / IPBAN / MUTE / TEMPMUTE / KICK / WARN with auto-expiry, audit trail, and a companion Velocity plugin that enforces on Mojang-verified login
PunishmentManagerholds canonical records plus an in-memory active-ban/mute cache keyed by UUID and IP; superseding writes automatically revoke prior active punishments of the same class; expiry loop deactivates tempbans/tempmutes every 30 s- Scope support (NETWORK / GROUP / SERVICE) — ban a player from a single group or a single service instead of the whole network. Multiple punishments can coexist against the same player (e.g.
BedWarsban + network-wide mute) - Console command
punishwith subcommandsban / tempban / ipban / mute / tempmute / kick / warn / unban / unmute / history / list(--group/--serviceflags for scope) - In-game command
/cloud punish …via the Bridge (honours the same permission nodes as the console command) - Mojang UUID lookup —
punish ban <name>resolves the real UUID through api.mojang.com, so staff can pre-ban players who have never joined the network. The same path is used byPOST /api/punishmentsfor dashboard-issued punishments - Editable kick/mute messages —
config/modules/punishments/messages.tomlwith placeholders ({target},{issuer},{reason},{remaining},{expires}). A dedicated WARN template is separate from KICK. Live-editable through the dashboard —GET/PUT /api/punishments/messagespersists atomically and becomes authoritative immediately, no restart - REST API:
GET /api/punishments,GET /api/punishments/{id},GET /api/punishments/player/{uuid}(history),GET /api/punishments/check/{uuid}?ip=&group=&service=(proxy login/connect check, cached),GET /api/punishments/mute/{uuid}?group=&service=,POST /api/punishments(accepts username or UUID, with scope + scopeTarget),DELETE /api/punishments/{id}(revoke with reason + actor) - Permission nodes:
nimbus.punish.ban,nimbus.punish.tempban,nimbus.punish.ipban,nimbus.punish.mute,nimbus.punish.tempmute,nimbus.punish.kick,nimbus.punish.warn,nimbus.punish.unban,nimbus.punish.unmute,nimbus.punish.history,nimbus.punish.bypass(skip mute enforcement) - Velocity punishments plugin (
plugins/punishments-velocity/) — auto-deployed to every proxy on prepare:LoginListener: denies login for NETWORK-scoped bans atLoginEvent(real Mojang UUID, post-auth)ConnectListener: blocksServerPreConnectEventfor GROUP/SERVICE-scoped bans — player stays on the network, just can't enter banned backendsLiveKickHandler: subscribes toPUNISHMENT_ISSUEDon the event stream so bans take effect instantly for already-connected playersPunishmentsApiClient: 5 s TTL caches for login / connect / mute checks
- Punishments backend plugin (
plugins/punishments-backend/) — minimal Paper/Spigot/Folia plugin auto-deployed to every backend: listens toAsyncPlayerChatEventand cancels it for muted players. Fetches mute state from/api/punishments/mute/{uuid}?group=&service=with a 3 s TTL cache to handle chat bursts; scope respects the backend's own service name + group. Exists because on Minecraft 1.19.1+ signed chat cannot be safely cancelled from Velocity — see Fixed below - Migrations:
PunishmentsV1_Baseline(v5000 — tables + indexes),PunishmentsV2_Scope(v5001 —scope+scope_targetcolumns; pre-existing rows default toNETWORK) - Events:
PUNISHMENT_ISSUED(carries a pre-renderedkickMessage),PUNISHMENT_REVOKED
-
Resource Packs module — Network-wide resource pack registry with URL-referenced and locally-hosted packs, prioritised assignment stacks, and a companion backend plugin that applies them on player join
- URL-referenced packs (
POST /api/resourcepackswith SHA-1) or locally-hosted packs (POST /api/resourcepacks/upload— raw-body streaming upload, SHA-1 computed during write, atomic rename withfd.sync()so a crash can't leave a partially-flushed file in the canonical location) - Assignment scopes: GLOBAL < GROUP < SERVICE, with priority ordering within each scope. A service sees all assignments for its name + group + global, sorted by
(scope-priority, priority) - Multi-pack stacks on 1.20.3+ — the backend plugin uses the multi-pack UUID API via reflection so you can layer GLOBAL base packs + GROUP overlays on modern clients; older servers fall back to the single highest-priority pack
- Public download endpoint
GET /api/resourcepacks/files/{uuid}.zip— unauthenticated because Minecraft clients don't send bearer tokens; the SHA-1 hash negotiated duringsetResourcePack()protects against tampering. Response streams through Ktor'srespondOutputStreamwith a 64 KiB transfer buffer so 250 MB packs don't spike server RAM - Telemetry — backend plugin reports
PlayerResourcePackStatusEventresults toPOST /api/resourcepacks/status, surfaced as theRESOURCE_PACK_STATUSevent on the event stream - Console command
resourcepack <list|add|upload|remove|assign|unassign|assignments> - Locally-hosted pack files live under
data/resourcepacks/<uuid>.zip - Backend plugin fetches
GET /api/resourcepacks/for-group/{group}?service=on player join with a 10 s local cache - Migration:
ResourcePacksV1_Baseline(v6000 — pack + assignment tables) - Events:
RESOURCE_PACK_CREATED,RESOURCE_PACK_DELETED,RESOURCE_PACK_ASSIGNED,RESOURCE_PACK_STATUS
- URL-referenced packs (
-
Module API —
PluginTargetfor proxy plugins —PluginDeploymentgained atarget: PluginTargetfield (BACKENDdefault,VELOCITYfor proxies). Modules can now register plugins for proxies too, not just backends (ServiceFactory.resolveModulePluginsiterates per-target on every service prepare) -
Mojang profile API lookup helper —
MojangUuidLookupresolves a username to a canonical(uuid, name)pair viaapi.mojang.com, used by the Punishments REST API and thepunishconsole command so staff can name-ban players who have never joined the network -
Dashboard — Punishments page — tabs for active vs history, inline filter, per-type badge styling, mc-heads avatars, revoke button with confirmation, New dialog with username-or-UUID input, type selector (all seven kinds), duration field for TEMPBAN/TEMPMUTE, target IP for IPBAN, and scope picker (NETWORK / GROUP / SERVICE with a scope-target field). Messages tab with per-type textareas, placeholder reference, and save/reload buttons — changes go live immediately, no restart
-
Dashboard — Resource Packs page — URL add dialog with Name / URL / SHA-1 / Prompt / Force fields and inline validation; separate Upload dialog with file picker, auto-populated name, prompt + force flags, and an HTTPS-proxy fallback for dashboards served over HTTPS against HTTP controllers; assignment manager with scope + priority; per-pack remove buttons
-
Dashboard — PlayerSheet integration — the player detail drawer now has collapsible Punishments (active badge count, auto-opens when entries exist) and Session history sections. Read-only — the issue/revoke workflow lives on the dedicated Punishments page
Changed
- BREAKING: Templates are fully user-owned —
templates/global/plugins/andtemplates/global_proxy/plugins/no longer receivenimbus-sdk.jar,nimbus-bridge.jar, or any module-provided plugin. All of them are now deployed at runtime on every service prepare viaServiceFactory.resolveModulePlugins(), usingREPLACE_EXISTINGso deleted/modified files self-heal on next start. Onlybridge.jsonconfig and Bedrock (Geyser/Floodgate) plugins remain written into templates. Migration for existing installs: delete the Nimbus-managed JARs from yourtemplates/global/plugins/andtemplates/global_proxy/plugins/directories (anything matchingnimbus-*.jar); they will be re-deployed on the next service start. If you leave them in place they'll simply be overwritten modulesconsole command no longer offers to deploy plugins into templates — the oldofferPluginDeploypath has been removed. Module plugins are runtime-deployed per serviceServiceFactory.preparenow reuses the lowest-numbered CRASHED/STOPPED slot instead of advancing to fresh numbers, so a crashedLobby-1staysLobby-1across respawn cycles (important for sync canonical state keyed by name)- Dashboard — dropped
@hugeicons/*,@phosphor-icons/react, and@tabler/icons-react; all icons now flow through@/lib/iconsbacked bylucide-react. Smaller bundle, consistent stroke weights
Fixed
- Signed-chat disconnect on mute — cancelling
PlayerChatEventfrom a VelocityEventTask.asynchandler on Minecraft 1.19.1+ races signature verification and the client sees "A Proxy Plugin caused an illegal protocol state" (kicked instead of muted). Chat mute enforcement moved to the newpunishments-backendplugin, which cancelsAsyncPlayerChatEventsynchronously — the correct place to deny signed chat. The Velocity-side chat listener is gone - Duplicate mute notification — previously both the Velocity plugin and the backend plugin sent the mute warning; now only the backend plugin does (which is also the one actually blocking the message)
- Mute cache never populated —
ChatListenerpre-fetch andLiveKickHandlerrefresh both calledserver.getScheduler().buildTask(new Object(), …). Velocity's scheduler requires a registered@Pluginas task owner — passing a plainObjectmakes the call throw, the task never runs, the cache stays empty, and the sync chat handler falls through to allow. Swapped both call sites toCompletableFuture.runAsync - Punishments by username never matched real players — dashboard-issued punishments fell back to
00000000-…whentargetUuidwas omitted.POST /api/punishmentsnow accepts explicit UUIDs, UUID-shaped strings in thetargetNamefield, or plain usernames (resolved viaMojangUuidLookupoff the IO dispatcher). Unknown names return404with a clear message, and canonical Mojang casing is echoed back so audit entries stay consistent - WARN reused the KICK template — WARN now has its own template (
"&e&l⚠ Warning &7from &f{issuer}\n&7Reason: &f{reason}"by default), separately editable in the dashboard - Resource pack upload returned 401 — the dashboard client was reading the token from the wrong localStorage key and sending
multipart/form-data, but the route expectsapplication/octet-stream. Client switched to the sharedapiUpload()helper (correct key + HTTPS-proxy fallback); route usescall.receiveStream()to match theModpackRoutespattern — no multipart parsing, no in-memory buffering of 250 MB packs
[0.8.2] - 2026-04-13
Hardening release: 14 cross-verified audit findings resolved across resilience, security, and operational safety. All fixes are backwards-compatible — existing configs work without changes.
Added
- Service health monitor — Background job detects stuck READY services that stop reporting health. Configurable via
controller.service_stale_timeout(default 300s,0to disable) - Configurable metrics retention — New
[metrics] retention_daysconfig (default 30). Previously hardcoded - Release checksum verification — GitHub release workflow now generates and uploads
SHA256SUMS.txtalongside JAR artifacts, enablinginstall.shintegrity verification - Log archive retention —
LogRotationnow prunes old.log.gzarchives, keeping the newest 30 by default - Port cache invalidation — External port occupancy cache now clears every 10 minutes so freed ports are rediscovered without restart
Changed
- EventBus overflow strategy — Changed from
SUSPEND(blocks emitter) toDROP_OLDESTto prevent cascading backpressure under extreme event load - Group reload is rollback-safe —
POST /api/reloadnow builds new groups in a local map first; if any group fails to construct, the entire reload is aborted and previous configuration preserved - Config parse error summary — Failed group and dedicated config files now produce a clear WARNING summary (
Loaded X config(s), Y failed: [filenames]) instead of only per-file ERROR logs
Fixed
- HTTP client hangs indefinitely — All 7
HttpClient(CIO)instances (JavaResolver, SoftwareResolver, UpdateChecker, ModpackInstaller, ControllerInfoRoutes) now have explicit connect/request/socket timeouts. Large JDK downloads get 10 min, API calls 30s - Service stuck in limbo after monitor exception — Exit monitor catch block now cleans up resources (port, process handle, registry, working directory) and transitions service to CRASHED instead of just logging
- Hard exitProcess(1) on database failure — Replaced with
DatabaseInitExceptionthat propagates naturally, allowing shutdown hooks to run and resources to clean up - MySQL migration lock timeout ignored —
GET_LOCKreturn value is now checked; throwsMigrationExceptionif lock cannot be acquired within 30s - Single bad cluster message kills agent connection — Per-message exception handling in WebSocket loop. Malformed messages are logged and skipped instead of disconnecting the entire node
- Player name injection in network commands —
/api/players/{name}/sendand/api/players/{name}/kicknow validate player names against^[a-zA-Z0-9_]{1,16}$before passing to Velocity commands - OOM on large file hashing — SHA-256 functions in ServiceDeployer, ServiceManager, and TemplateRoutes now stream with 64 KB reads instead of
Files.readAllBytes(), preventing heap exhaustion on large modded worlds/templates - Audit log pruning locks database — Replaced single-transaction
DELETEwith batched deletion (5,000 rows per batch, 100ms yield between batches), matching the existing MetricsCollector pattern
[0.8.1] - 2026-04-13
Stability fixes: 15 production-critical findings resolved plus a cluster TLS keystore bug that prevented restarts.
Fixed
- OOM on large downloads — Modpack imports and server software downloads now stream to disk instead of loading entire files into memory
- HTTP client resource leak — Modpack installer and template downloader now properly close their HTTP clients after use
- Unbounded collector queues — Metrics and audit collectors capped at 10,000 pending events; excess events are dropped with a warning instead of consuming unbounded memory
- HikariCP connection pool leak — Database connection pool now properly closes on shutdown instead of leaking connections
- SQLite "database is locked" under load — Added 5-second busy timeout after WAL mode to handle concurrent writes gracefully
- Port scan exhaustion — External port occupancy check results are now cached instead of probing sockets on every service start
- Scale-down kills services abruptly — Services now enter DRAINING state before being stopped, allowing in-progress work to complete
- State file corruption under concurrent writes — Controller state store now uses a read-write lock for atomic read-modify-write operations
- ConcurrentModificationException in scaling engine — Service map keys are now snapshot before iteration to prevent modification during scale-down evaluation
- Stale service data after concurrent updates —
merge()return values are no longer used for control flow; map is re-read after mutation - Invalid memory values accepted in group config — Memory now validated to be between 128M and 512G at config load time
- Circular template references cause infinite loop — Template stacking now tracks visited templates and errors on cycles
- Silent coroutine crashes — Uncaught coroutine exceptions now logged via SLF4J instead of vanishing silently
- Retention cleanup blocks event loop — Metrics and audit retention pruning now deletes in batches of 5,000 with cooperative yields between batches
- Cluster TLS keystore fails on restart — Keystore password was randomly generated but never persisted, causing "keystore password was incorrect" on every restart. Now derives a deterministic password from the cluster token via SHA-256 and auto-regenerates the keystore if the password doesn't match
- False "Cluster started" log —
ClusterStartedevent now only fires when the cluster server is actually running
[0.8.0] - 2026-04-12
Production readiness audit: 65 findings resolved across security, stability, data integrity, and performance.
Added
- HikariCP connection pool — MySQL and PostgreSQL connections now use HikariCP with configurable pool size (default 10), connection timeout (30s), and validation timeout (5s). SQLite unchanged
- Distributed migration lock —
MigrationManageracquires advisory locks on MySQL/PostgreSQL before running migrations, preventing concurrent controller starts from racing on schema changes - Rate limiter X-Forwarded-For support — New
api.trust_forwarded_forconfig option. When behind a reverse proxy, rate limiting keys on the real client IP instead of the proxy address - Configurable scaling tick interval — New
controller.scaling_tick_intervalconfig (default 10s) for game modes needing faster scaling reaction - Install script restart on exit code 10 —
install.shandinstall.ps1detect exit code 10 and re-launch automatically, enabling zero-downtime auto-updates - Install script SHA-256 verification —
install.shverifies JAR checksum from release assets when available - Human-readable uptime —
GET /api/statusincludesuptimeHumanfield (e.g., "2h 15m 30s") - Bridge JAR version-stamped — Embedded JAR renamed to
nimbus-bridge-<version>.jarfor identification - V5 migration — Widens all timestamp columns from VARCHAR(30) to VARCHAR(40) for nanosecond-precision ISO-8601
- V6 migration — Adds metrics composite index, widens actor column to VARCHAR(255), upgrades high-volume table IDs to BIGINT
Changed
- CORS default restricted — Default
allowed_originschanged from["*"]to dashboard + localhost only - ProxySyncManager debounced — MOTD and tab list broadcasts debounced with 500ms delay to prevent flooding under rapid player churn
- Agent exponential reconnect backoff — Reconnect delay escalates from 1s to 60s (1→2→4→8→16→32→60) instead of fixed 5s retry flood
- Reload command notes restart requirement — Output now includes "Database, API, and cluster config changes require a full restart"
- WarmPool respects global service cap — Pre-staging checks total service count against
controller.max_services - Smart Scaling accounts for warm pool — Predictive warmup target subtracts
warm_pool_sizeto avoid double-counting - ServerListPing timeout raised — Default from 3s to 5s for slow VMs
- Pufferfish versions filtered — Versions older than 1.19 filtered from CI listing
Fixed
- WebSocket deadlock under concurrent sessions —
runBlockinginside Ktor WebSocket handlers replaced with channel-based output. 10+ concurrent Remote CLI sessions no longer exhaust the thread pool - Dropped READY signals during mass startup — Thread-safe set for
awaitServicesReadyprevents missed READY events during 20+ simultaneous service starts - EADDRINUSE on rapid restarts —
restartServicenow polls for STOPPED/CRASHED state (up to 60s) before starting the new instance - Silent template data loss on deploy-back failure — Deploy-back now runs before state transition. If it fails, service stays in CRASHED state with a descriptive error
- Dead backends stuck in load balancer — Load balancer now subscribes to
ServiceCrashedevents alongsideServiceStopped - Scale-down blocked by pending services — Pending services only block scale-up evaluation, not scale-down
- Module disable crash takes down shutdown —
disableAll()catches Throwable (not just Exception) to match init/enable behavior - ConcurrentModificationException during node registration — Thread-safe map for remote service handles
- Log spam from misbehaving server MOTD — Malformed JSON caught with backoff (3 consecutive failures = skip)
- Collector init before migration — Metrics, audit, and CLI session collectors now start after migrations complete. Fresh installs no longer crash on first event
- State sync skipped on bulk shutdown —
stopAll()now calls state push for each service before clearing handles - Agent state file corruption — Synchronized read-modify-write on
services.json - Timestamp truncation — Nanosecond-precision timestamps no longer truncate in VARCHAR(30) columns
- Audit actor truncation — Actor column widened from VARCHAR(50) to VARCHAR(255)
- INT overflow on high-volume tables — Audit, events, metrics, and scaling table IDs upgraded to BIGINT
- Auto-update accepts corrupt downloads — Downloaded JARs now verified via ZIP validation before accepting
- ATOMIC_MOVE fails across filesystems — Copy+delete fallback when
/tmpand data dir are on different volumes - WarmPool not cleaned on group delete — PREPARING slots released when a group is disabled
- Bootstrap URL double port —
cluster bootstrap-urlno longer appends redundant port whenpublic_hostalready includes one - Dashboard login "Network Error" — Replaced with troubleshooting hints (check address, controller running, port open)
- Cardboard dependency failure silent — iCommon download errors now logged
- Database connection error opaque — Failed connections now log a clear error with config hints and exit cleanly
- GeoIP blocks CPU coroutines — Lookup moved to IO dispatcher
Security
- Tab completion endpoint unauthenticated —
POST /api/console/completenow requires Bearer token auth - Blank cluster token bypasses auth — Template download and agent WebSocket endpoints now reject blank tokens
- Path traversal on modpack upload — File names sanitized with canonical path validation
- Hardcoded keystore password — Replaced with auto-generated random 32-char password on first run
- Cluster token in URL query strings — State sync requests now use Authorization header. Tokens no longer appear in access logs
- UUID injection on proxy events —
POST /api/proxy/eventsvalidates UUID format before downstream use - JWT bypass with dotted API tokens — Token type detection now decodes Base64 header and checks for
"alg"field - ZIP bomb on template extraction — Capped at 50,000 entries and 10 GB total
Compatibility
- Agent tolerates unknown config keys — Adding new config fields in future versions won't crash existing agents
- Windows Java auto-detection — Scans Program Files for Java, Eclipse Adoptium, Microsoft, BellSoft, Amazon Corretto, and Zulu installations. Respects JAVA_HOME/JDK_HOME
- Agent dumb terminal warning — Non-interactive terminals (systemd, Docker) no longer show JLine warnings
- TOML line endings on Windows — Config writes force LF to prevent parse warnings
- Version fallback for IDE builds — Reads version from
gradle.propertieswhen JAR manifest is unavailable - Agent directory permissions — Install script sets 750 instead of world-writable
[0.7.3] - 2026-04-12
Cluster hardening: orphan sweep, reconnect reconciliation, graceful shutdown ordering, daemon mode, and failover.
Added
- Marker-file orphan sweep — Spawned backends write
.nimbus-ownermarkers. On recovery, orphaned processes are detected and killed based on these markers instead of unreliableProcessHandle.commandLine() - Reconnect reconciliation — Agents send their running service list on every reconnect. Controller purges ghost entries the agent doesn't claim
POST /api/shutdownendpoint — REST endpoint for daemon-mode controllers with no TTY- Daemon mode — Controller detects headless environment and stays alive without JLine. Works under systemd, Docker, or
cmd.exe /c - Agent public host auto-detection — Agents detect their real host IP (skipping Hyper-V, VMware, WSL, Tailscale interfaces) and report it to the controller
- Agent-crash failover — Controller detects node failure and re-places affected services on available nodes
- Railway-style cluster topology — Interactive dashboard canvas with draggable nodes, heartbeat pulse animations, pan/zoom, and auto-fit
Changed
- Graceful shutdown ordering —
ShutdownAgentsent to all nodes before closing the cluster WebSocket server. Previously the server closed first, dropping queued frames - Memory budget raised — JVM overhead estimate raised from 30%/256MB to 50%/384MB to better reflect real-world RSS. Terminal states now report 0 MB
- State machine expanded — CRASHED services can now be stopped/restarted without a controller restart. CRASHED→STOPPING and PREPARING/STARTING→STOPPING transitions allowed
- Startup crash detection — Exit code 0 during STARTING is now treated as a crash (Paper/Velocity catch BindException and call System.exit(0))
- Rate limiting split —
GET /api/stressstatus read separated from mutation rate limit. Dashboards polling stress status no longer hit 429 - Scaling retry backoff — Exponential schedule (10s → 30s → 90s → 5min → 15min, capped) for repeated placement failures
Fixed
- Orphan cascade on agent hard-kill — Killing an agent left backend processes alive, holding session.lock. Each restart attempt spawned additional orphans. Fixed by marker-file sweep + exponential retry + dedup
- Ghost services after transient reconnect — WebSocket drop followed by fresh-state reconnect left stale READY entries blocking max_instances
- Dedicated services crash at boot — Port patching was missing for dedicated services, causing collisions with the proxy
- Duplicate dedicated-start on boot — Dedicated services no longer started twice during phased startup
- Node placement to APIPA addresses on Windows — Controller uses agent-reported public host instead of socket peer address
[0.7.2] - 2026-04-11
State sync simplified to graceful-stop-only persistence. Removes live-sync complexity.
Changed
- State sync model simplified — Reduced to push-on-stop / pull-on-start. Periodic snapshots, manual triggers, and flush coordination removed. Simpler and more reliable
- "Sync" badge renamed to "Persistent" in the dashboard
- Dynamic group sync disabled with warning —
[group.sync] enabled = trueon non-STATIC groups logs a warning and force-disables
Removed
- Snapshot interval, flush command, and flush wait config fields from
[group.sync] POST /api/services/{name}/sync/triggerendpointTRIGGER_SYNCcluster message and related SDK helpers- Dashboard "Sync now" action and in-flight sync UI
Fixed
- Sync push corrupted by active file writes — Files now snapshotted into memory at form-build time (up to 64 MB, larger files stream)
- "Invalid state transition X → X" log spam — Same-state transitions are now silent no-ops
- Windows agent reports 0 MB RAM — Added tasklist fallback for RSS reading
- Template download OOM on agents — Streams to disk with 64 KB buffer instead of loading entire ZIP into heap
- SHA-256 mismatches on sync push — Receiver accepts actual streamed bytes when files change between manifest scan and upload
- NTFS rename failure under Windows Defender — Retry with exponential backoff (100ms → 2s)
- Ready timeouts too short — Raised to 180s controller/agent, 240s modded
[0.7.1] - 2026-04-11
Sync hardening: health checks, quota enforcement, and reconciliation.
Added
- Sync health metrics — Per-service last push time, canonical size, and in-flight status on
GET /api/metrics - Sync quota — Configurable maximum canonical size per service
- Sync reconciliation — Controller detects orphaned state directories on startup
- Manual sync trigger —
POST /api/services/{name}/sync/triggerendpoint and SDK helpers - Dashboard sync indicators — Node ID column, sync health badge, "Sync now" action, global toast for placement/sync events
Fixed
- Scaling slot reuse race — Mutex + atomic register prevents two concurrent scale-ups from claiming the same service name
[0.7.0] - 2026-04-11
Multi-node cluster: easy TLS setup, placement pinning, dynamic state sync, and service name stability.
Added
- Easy cluster setup — Agent setup wizard fetches the controller's TLS fingerprint via
/api/cluster/bootstrap. SSH-style trust-on-first-use, no manual keytool or truststore work - Placement pinning —
[group.placement] node = "worker-1"pins services to specific nodes.fallback = wait | local | failcontrols offline behavior - Dynamic state sync —
[group.sync] enabled = truestores a canonical copy on the controller. Agent pulls delta on start, pushes on graceful stop. SHA-based delta detection - Service name stability — Lowest-numbered CRASHED/STOPPED slot reused instead of always incrementing.
Lobby-1staysLobby-1across crash-respawn cycles - Dedicated services on agent nodes —
[dedicated.placement]with mandatory state sync extra_sansandpublic_hostcluster config — For custom SANs and NAT/multi-interface setups- Cluster TLS & Security docs — Threat model, cert rotation, custom CA, troubleshooting
Changed
- Agent template downloads stream to disk — Prevents OOM on low-memory agents
- Cluster auth extended — Agents report hostname, OS, CPU, RAM, Java version, and running services on connect
Fixed
- TLS trust for template downloads — Agents can now download templates from TLS-enabled controllers
- Velocity routing to wrong addresses — Agents report real host IP instead of controller using WebSocket peer address
[0.6.2] - 2026-04-10
Overview-first dashboard with host system telemetry and cluster node metrics.
Added
- Controller host system stats — CPU model, cores, live CPU/RAM usage, Java version, and hostname exposed on
GET /api/controller/info - Cluster node system specs — Agents report hardware info at auth time. Nodes page shows system stats for every remote agent
- Overview page rebuilt — System stats cards, memory budget, uptime, "What's new" accordion with latest release notes
- Light/dark theme toggle — Respects system preference, remembers last choice
- Phosphor Icons — All dashboard icons migrated from Lucide to Phosphor Regular weight
Changed
- Agent heartbeat reports real system memory — Previously reported JVM heap instead of system-wide usage
Fixed
- Button crash when rendering as anchor — Base UI's Button primitive now correctly handles anchor elements
Removed
- Info sheet sidebar — Replaced by inline Overview cards
[0.6.1] - 2026-04-10
Dashboard design system pass, historical memory charts, and audit log fix.
Added
- Unified dashboard design system — Shared
PageHeader,StatCard,EmptyState,SectionLabel,MemoryBar, andSheetBodycomponents across all pages - Historical memory charts —
service_metric_samplestable (V4 migration) records RSS + player count every 30s. Service detail page shows last hour on load - Memory bar component — Inline progress bar with pressure-coded colors (green → yellow → red)
- Dashboard breadcrumbs now mirror sidebar labels exactly
Changed
- Accent color matched to logo — Color tokens moved from hue 210 (teal-cyan) to hue 235 (sky-blue)
- TPS hidden from dashboard — Only SDK-reporting services had TPS data. Dashboard now focuses on memory. TPS still tracked internally for health
- Audit log uses proper serialization — DTOs instead of untyped maps
Fixed
- Audit log empty in dashboard — API endpoint silently returned 500 due to kotlinx.serialization not supporting
Map<String, Any?>. Fixed with typed DTOs - Service detail memory chart too tall — Overridden aspect ratio to fixed height
- Display module edit sheet misaligned — Double-padding removed
- Service detail stat cards stacking on desktop — Forced to 3-column grid
[0.6.0] - 2026-04-10
Dedicated services: single-instance, fixed-port servers plus unified memory reading.
Added
- Dedicated services — Single-instance, fixed-port servers in managed directories. No template required. Auto-downloads server JAR on first start. Config via
config/dedicated/*.toml - Dedicated REST API — Full CRUD at
/api/dedicated/*plus modpack import/upload endpoints dedicatedconsole command — List, create, start, stop, restart, delete, info with interactive creation wizard- Dashboard dedicated page — Create, configure, modpack import, and management actions
- Controller info API —
GET /api/controller/inforeturns version, uptime, memory budget, and update status - Unified memory reading — RSS read from
/proc/<pid>/status(Linux/WSL) ortasklist(Windows) for every service. No longer requires SDK integration - Agent memory heartbeats — Cluster agents push RSS in heartbeats for accurate remote service memory
- Dashboard sidebar redesign — Nav split into labeled sections: Overview, Infrastructure, Operations, Monitoring, Modules
Changed
- Proxy forwarding mod sync on every start — Cleans up stale mods from wrong modloaders automatically
- NeoForge mod family auto-swap — Pre-1.20.2 and 1.20.2+ forwarding mods swap automatically when software version changes
- SDK simplified —
reportHealth()only sends TPS now. Memory is the controller's job. Backwards-compatible
Fixed
- Non-SDK services showed 0 memory — All services now report real resident memory
- Controller info showed JVM heap instead of services budget — Now reads
controller.max_memoryfrom config - RSS exceeds displayed max — JVM overhead budget added to displayed maximum
- Dedicated service name mismatch — URL path is now authoritative over request body
[0.5.3] - 2026-04-09
Smart modded client routing via mod list matching.
Added
- Mod list matching — Proxy routes modded clients to the group with the best mod overlap score (
|clientMods ∩ serverMods| / |serverMods|, threshold 0.5) - ModScanner — Extracts mod IDs from template
mods/directories on group load. Supports NeoForge/Forgemods.toml, Fabricfabric.mod.json, and legacymcmod.info - Protocol version filtering — Client protocol version must match the group's MC version
- Connection type compatibility —
LEGACY_FORGEmatches Forge only;MODERN_FORGEmatches Forge or NeoForge - Alternate group fallback on kick — Modded clients try a different modded group before disconnecting
Changed
GET /api/groupsnow includesmodIdsfield
[0.5.2] - 2026-04-09
Auto-configure Velocity for large modpacks.
Fixed
- Silent disconnects with large modpacks — Auto-sets
-Dvelocity.max-known-packs=512when modded backends exist. Default limit of 64 caused disconnects with 400+ mods
[0.5.1] - 2026-04-09
Modded client routing hotfixes.
Fixed
- NeoForge forwarding errors — NeoForge 1.20.2+ now uses NeoForwarding instead of proxy-compatible-forge, which caused
Empty keyerrors - Modded client detection timing — Uses Velocity's
ConnectionType(set during FML handshake) instead ofplayer.getModInfo()which was empty at initial server selection - Modded groups API parsing — Bridge correctly handles the
{"groups": [...]}wrapper - Kick loop for modded clients — Modded clients kicked from a modded server no longer redirect to a vanilla lobby
Changed
- Config patching auto-detects forwarding mod — Patches the correct config file based on which mod is present
[0.5.0] - 2026-04-09
Modded client support: automatic routing and Velocity configuration for Forge/NeoForge/Fabric players.
Added
- Modded client routing — Bridge detects Forge/NeoForge/Fabric clients and routes them to modded backend groups instead of the Paper lobby
- Automatic Velocity modded config — Detects modded backends and auto-sets
announce-forge = true, increases connection/read timeouts for large modpack handshakes - Modded groups cache — Bridge fetches group software types from API and refreshes on reconnect. No proxy restart needed when modded groups are added
Fixed
announce-forgeplacement — Correctly placed at TOML root level matching Velocity's default config structure
[0.4.7] - 2026-04-09
Module startup ordering fix.
Fixed
- Permissions module crash on fresh installs — DB queries moved from
init()toenable(), which runs after migrations - Module lifecycle ordering — Split into
initAll()(registers migrations) → migrations run →enableAll()(DB-dependent setup)
[0.4.6] - 2026-04-09
Module loading hotfix — bypasses broken ServiceLoader.
Fixed
- Module loading on certain Java 21 configurations — Modules now declare
main_classinmodule.propertiesand load viaClass.forName(), bypassing unreliable ServiceLoader - Version-incompatible modules still loaded — Skip logic now correctly removes modules before loading
- Silent Error-class crashes —
catch (Throwable)instead ofcatch (Exception)to surface NoClassDefFoundError and similar
[0.4.5] - 2026-04-09
Intermediate module loading diagnostics (superseded by v0.4.6).
[0.4.4] - 2026-04-09
NeoForge symlink fix and module loading improvements.
Fixed
- NeoForge startup on Linux —
Files.walk()now follows symlinks solibraries/is found through symlinked directories - Module version skip logic — Skipped modules properly removed before ServiceLoader runs
- Module error handling —
catch (Throwable)to surface linkage errors
[0.4.3] - 2026-04-09
Dashboard proxy and chunked modpack upload.
Added
- Server-side proxy — Hosted dashboard can connect to HTTP controllers without TLS certificates
- WebSocket SSE bridge — Server-Sent Events bridge for WebSocket connections through the proxy
- Chunked modpack upload — Large uploads (up to 2 GB) split into 4 MB chunks with zero RAM buffering on client, proxy, and controller
Changed
- Login simplified — Only requires IP/hostname. Port defaults to 8080
[0.4.2] - 2026-04-09
CORS configuration hotfix.
Fixed
- CORS missing on new installs — SetupWizard now includes
allowed_originsin generated config - Unknown config keys crash — TOML parser now tolerates unknown keys for forward compatibility
- Wrong key name in docs — Dashboard guide referenced
cors_originsinstead ofallowed_origins
[0.4.1] - 2026-04-08
CurseForge modpack support and server pack ZIP import.
Added
- CurseForge modpack support — Import via slug, URL, or server pack ZIP upload. Optional
[curseforge] api_keyin config - Server pack ZIP import — Upload pre-built server packs via CLI or dashboard. Auto-detects modloader and MC version
- Dashboard modpack upload — Drag-and-drop file picker in the Import Modpack dialog
Changed
- Import command now accepts CurseForge URLs,
curseforge:slugprefix, and.zipfiles - Memory input normalization — Bare numbers auto-suffixed (e.g.,
8→8G,512→512M)
Fixed
- NeoForge startup on Windows — Directory junctions as fallback when symlinks fail (no admin rights required)
- NeoForge installation detection — Recognizes modern installations using
libraries/with args files
[0.4.0] - 2026-04-08
Web Dashboard: manage your cloud from the browser.
The Web Dashboard is currently in ALPHA. Features may change, and bugs are expected.
Added
- Web Dashboard (ALPHA) — Next.js + shadcn/ui management UI at dashboard.nimbuspowered.org:
- Service overview with live status, player counts, uptime
- Group management with create/configure flows
- Real-time console per service with ANSI rendering
- Plugin management (Hangar + Modrinth search and install)
- Modpack import (Modrinth, CurseForge, ZIP upload)
- Stress testing controls
- Audit log viewer with filters
- Node management for multi-node clusters
- Module configuration (Display, Permissions, Players, SyncProxy)
- Metrics visualization with charts
- Responsive design (desktop + mobile)
- Modpack import API —
POST /api/modpacks/import,GET /api/modpacks/platforms - Plugin management API —
GET /api/plugins/search,POST /api/plugins/install,GET /api/plugins/installed/{group} - Software versions API —
GET /api/software,GET /api/software/{type}/versions
[0.3.1] - 2026-04-07
Remote CLI: manage your cloud from anywhere.
Added
- Remote CLI (
nimbus-cli) — Standalone JLine3 console that connects to the controller's API. Same commands, tab completion, and ANSI formatting as the local console. Interactive setup wizard, saved connection profiles in~/.nimbus/cli.json - Console API endpoints —
POST /api/console/completefor tab completion,WS /api/console/streamfor multiplexed command execution and event streaming - CLI session tracking — Connections logged in DB with client username, hostname, OS, IP, GeoIP location, duration, and command count
sessionscommand —sessions activeandsessions historyto monitor Remote CLI connections- CLI connection events — Live events in local console showing who connected, from where, with OS details
- Install scripts —
install-cli.shandinstall-cli.ps1for one-command CLI installation
Changed
- All console commands refactored to
CommandOutputinterface for remote execution support
[0.3.0] - 2026-04-07
Competitive features: closing the gaps with SimpleCloud and CloudNET.
Added
- Template stacking —
templates = ["base", "overlay"]in group config, applied in order. Later templates override earlier ones. Full cluster support - Warm pool — New PREPARED service state with background replenishment. Configured via
scaling.warm_pool_size. Prepared services start in seconds instead of minutes - Player module (
nimbus-module-players) — Centralized player tracking with DB persistence, REST API, console commands, and Bridge integration - Service deployments — Hash-based copy-back of changed files from service to template on stop. Configured via
lifecycle.deploy_on_stopandlifecycle.deploy_excludes - Remote file management API — Browse, read, write, delete files on agent-hosted services via REST with path traversal protection
- Module auto-sync — Embedded module JARs automatically updated from build on startup
[0.2.1] - 2026-04-07
Bug fixes, console feedback, and config validation.
Fixed
- Console command feedback —
start,stop,restartreport actual success/failure with details - Silent memory parsing — Invalid cluster node memory formats now logged instead of silently returning 0
Changed
- Config validation on startup —
controller.max_memory,bedrock.base_port, anddatabase.portvalidated at load time - Scaling debug logging — Cooldown and pending-service skips logged at DEBUG level
- Proxy sync error logging — Full stack traces on TOML parse failures
[0.2.0] - 2026-04-07
Security, stability, and production readiness.
Added
- Database migration system — Versioned schema changes with auto-bootstrap for pre-0.2.0 databases. Module migrations via
ModuleContext.registerMigrations() - Audit logging — EventBus-driven batch writes to
audit_logtable. Actor tracking, console command, REST endpoint, configurable retention (default 90 days) - Environment variable overrides —
NIMBUS_API_TOKEN,NIMBUS_DB_*,NIMBUS_CLUSTER_TOKEN,NIMBUS_AGENT_TOKEN,NIMBUS_AGENT_CONTROLLERoverride TOML config - Cluster TLS — Auto-generated self-signed keystore,
wss://default. Configurable trust viatls_verify,truststore_path - Local PID persistence — Services survive controller restarts via
state/services.json - State reconciliation — Grace period before starting minimum instances, allows agents to report recovered services
- JWT scoped API auth (opt-in) — HMAC-SHA256 tokens with 13 granular permission scopes
- Leaf server software — Paper fork from
api.leafmc.one - Interactive pickers — Arrow-key selection replacing Y/N prompts
Changed
- Org rename —
KryonixMCtoNimbusPowered, packagedev.nimbuspowered.nimbus - Cluster default protocol —
wss://instead ofws://(breaking) - Docs migration — VitePress replaced with Fumadocs (Next.js)
Fixed
- Migration bootstrap edge cases on upgrade
- PostgreSQL port 3306 override now throws an error
- Windows file-lock tolerance in template manager
[0.1.4] - 2026-04-05
API polish and health monitoring.
Added
- Machine-readable API errors — Standardized codes like
SERVICE_NOT_FOUND,VALIDATION_FAILED healthcommand — Overview of service health across all groups/api/services/healthendpoint — Aggregated health summary per group- Bridge health integration — Velocity plugin reports health status
Changed
- Smart Scaling module set to opt-in (not pre-installed during setup)
[0.1.3] - 2026-04-05
Java baseline bump and operational improvements.
Added
versioncommand — Shows running version, checks for updates- JDK auto-download — Multi-provider fallback (Adoptium, Azul, Oracle) with streaming progress
- Smart start scripts — Auto-restart after update
Changed
- Java 16+ baseline — Dropped Java 8/11 support
[0.1.2] - 2026-04-05
Smart scaling and module extraction.
Added
- Smart Scaling module — Time-based schedules, predictive warmup from 7-day player history
- Phased startup — Proxies must reach READY before backends start
- Dynamic Bridge commands — Controller pushes command registry to Velocity at runtime
Changed
- Permissions and Display logic fully extracted into standalone module JARs
[0.1.1] - 2026-04-04
Module system architecture and security hardening.
Added
- Module system — Dynamic loading with lifecycle hooks (
init/enable/disable), dependency resolution, version compatibility checks, embedded module auto-extraction modulescommand — Install, uninstall, list, infopluginscommand — Live search across Hangar + Modrinth with version matching and dependency resolutionkick/broadcastcommands- Least-players lobby balancing
Changed
- Package rename from
dev.nimbustodev.kryonix.nimbus - ProtocolLib dependency removed (API-only approaches)
Fixed
- Zip slip vulnerability in agent template extraction
- Scaling deadlock when all group instances crash simultaneously
- Nametag MiniMessage rendering
Security
- Security hardening audit across all input boundaries
- Agent template extraction validates against directory traversal
- API input sanitization for command injection prevention
[0.1.0] - 2026-04-01
Initial public release — the complete Minecraft cloud system, built from scratch.
Core
- Service lifecycle: start, stop, restart, crash detection, auto-restart
- Server groups with TOML configuration
- Port allocation: 25565+ for proxies, 30000+ for backends
- Static and dynamic services, graceful shutdown ordering
- Player-count-based auto-scaling with configurable thresholds and cooldowns
Server Software
- Paper, Purpur, Pufferfish, Leaf, Folia, Forge, NeoForge, Fabric, Custom
- Auto-download from official sources, EULA auto-acceptance
- ViaVersion/ViaBackwards/ViaRewind support with dependency enforcement
Networking
- Velocity auto-management with modern + legacy forwarding
- Proxy sync: MOTD, tab list, chat formatting, maintenance mode
- TCP load balancer with round-robin/least-players, PROXY protocol v2
- Geyser + Floodgate for Bedrock support
API & Console
- REST API v0.2 with bearer token auth, WebSocket live events, bidirectional console
- 30+ JLine3 console commands with tab completion and ANSI formatting
- Rate limiting: 120 req/min global, 5 req/min stress endpoints
Infrastructure
- SQLite/MySQL/PostgreSQL via Exposed ORM with versioned migrations
- Multi-node cluster via TLS WebSocket with placement strategies
- One-command installers for Linux/macOS/Windows
- GitHub Releases auto-updater with semver comparison
Plugins
- Nimbus SDK (Spigot 1.8.8+ through latest Paper/Folia)
- Nimbus Perms (builtin or LuckPerms provider)
- Nimbus Display (signs + NPCs via FancyNpcs)
- Nimbus Bridge (Velocity hub commands + cloud integration)
- Performance optimizer: Aikar's JVM flags + Paper/Purpur config tuning
- Stress testing with simulated player load