Nimbusv1.0.0

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_config default flipped to true — unknown keys in nimbus.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.players permission node removed — the auto-migration that added nimbus.cloud.players alongside the old node (introduced in 0.13.0) now only writes the new node. Groups that still reference nimbus.players directly will no longer match.
  • Module API compat shims removed — the @Deprecated type aliases in dev.nimbuspowered.nimbus.module pointing to dev.nimbuspowered.nimbus.module.api are 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 @Deprecated forwarding aliases (NimbusModule, ModuleContext, ModuleCommand, AuthPrincipal, and all other public module API types) that pointed to their dev.nimbuspowered.nimbus.module.api.* counterparts are removed. Third-party module authors must update their imports to the module.api package before upgrading to 0.14.0.
  • object ApiErrors compat facade — the deprecated ApiErrors string-constant object that delegated to the ApiError enum is removed. Any code referencing ApiErrors.* directly must switch to ApiError.* or the typed enum values.
  • nimbus.players permission node support — the old node no longer triggers a match in the permission engine. Any groups still referencing it should be updated to nimbus.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 of token) and forward-compatible downgrade changes that were previously ignored without feedback. Opt-in fail-mode via [controller] strict_config = true — unknown keys become a ConfigException startup abort instead of just a warning. Default in 0.13 is false (warn-only); 1.0 flips the default to true. 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 handshakeClusterMessage.AuthRequest and AuthResponse now carry an explicit protocolVersion field (currently CURRENT_PROTOCOL_VERSION = 1). During the handshake the controller compares the agent-reported version with its own before checking the token. A mismatch produces AuthResponse(accepted = false, reason = "protocol version mismatch: agent=X, controller=Y") and a WS close with VIOLATED_POLICY. On the agent side, AgentRuntime detects 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) plus ignoreUnknownKeys = true in clusterJson preserve 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 @SerialName message 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 extensions hasPermission and service<T>()) have been moved from dev.nimbuspowered.nimbus.module to dev.nimbuspowered.nimbus.module.api. Goal: cleaner separation between the public module API and core-internal types (ModuleManager, ModuleContextImpl, ModuleInfo).
  • ApiError enum consolidation — BREAKING — the loose ApiErrors string constants are now consolidated into a typed enum class ApiError(val code: String, val defaultStatus: HttpStatusCode) that all routes use uniformly for error codes and default HTTP status. object ApiErrors is retained as a @Deprecated compat 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) and BACKUP_* for all backup module errors, plus existing core codes. Removed the unused INSUFFICIENT_SCOPE code. No changes needed on the API consumer side.
  • Permission node rename — BREAKINGnimbus.players is now nimbus.cloud.players, consistent with the rest of the nimbus.cloud.* admin family. Auto-migrated on startup: PermsModule.enable() calls the new PermissionManager.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 = true in nimbus.toml. A new complete permission registry in docs/reference/permissions.mdx documents all three families: nimbus.cloud.* (admin/operations), nimbus.dashboard.* (web UI), and nimbus.<module>.<behavior> (in-game gameplay).
  • SDK public surface frozen — the stable API surface of plugins/sdk is now documented and guarded against accidental changes. Classes without @ApiStatus.Internal form 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. New package-info.java files in sdk/, sdk/compat/, and sdk/event/ document the contract at the package boundary. A new SdkPublicSurfaceTest regression 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.mdx documents the SemVer guarantees for the /api/* URL space (additive minor changes, removals/breaks in major versions only), the Deprecation: header path including sunset window, the status of error codes (ApiError enum) 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.actor grammar — the actor: String field on NimbusEvent now has explicit KDoc grammar: either a bare type or <type>:<id>, with type ∈ { "system", "console", "api", "player", "agent" }. New extension NimbusEvent.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> to dev.nimbuspowered.nimbus.module.api.<Type>. The compatibility shim is removed in 0.14.0.
  • Permission node nimbus.playersnimbus.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 via systemd-run --user --scope, with kernel-enforced MemoryMax, CPUQuota, and TasksMax. Default default_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 optional memory_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 the ServiceCrashed event and the new Service.lastCrashReport field. 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), stale session.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/metrics endpoint: 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 — Prometheus rate() / increase() handles resets natively, and persisting these would just duplicate what MetricsCollector already keeps in the DB for long-running dashboard charts
  • Structured reload reportPOST /api/reload and the console reload command now return/display a ReloadReport naming every known config section with its ReloadScope (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 legacy success, groupsLoaded, and message fields 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 edited nimbus.toml and nothing changed" confusion that the previous one-line hint at the end of reload output always missed
  • Opt-in JSON logging — set NIMBUS_LOG_FORMAT=json in the environment before starting the controller to switch from the default plain-text latest.log pattern to one-object-per-line JSON via logstash-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 future MDC.put(...) site starts emitting rich fields without further config changes. Default plain-text mode is unchanged
  • Unknown server-version hard-fail + NeoForwarding bootstrapSoftwareResolver now throws UnknownServerVersionException instead 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). CreateGroupCommand surfaces 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 the nimbus.toml reference and the group-config guide. The /api/reload response example updated to show the new ReloadReport shape with legacy-field compatibility, and the /api/metrics section lists the five new counter/gauge series

Fixed

  • CompatibilityChecker no longer crashes on groups with unresolvable software versions — the upstream UnknownServerVersionException is 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.StartService schema 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 own PUT /api/backups/config endpoint for hot-reload
  • MDC emit sites — the JSON config already whitelists the keys; future MDC.put calls in ServiceManager and 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 check systemctl --user list-units | grep nimbus- — you should see a *.scope unit per running service
  • nimbus_service_crashes_total infers 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-core suite 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 to dev/nimbuspowered/**. Baseline coverage: line 12.5 %, branch 7.7 %, method 14.9 %, class 15.6 %. Top-covered modules: nimbus-protocol 88 %, display 40 %, backup 33 %, auth 29 %, resourcepacks 29 %, punishments 28 %, players 28 %

Changed

  • Dev tooling — Java plugin subprojects now scope -source 16 -target 16 to compileJava only, so compileTestJava can use Java 21 features. A handful of TOTP / scaling helpers were promoted from private to internal so 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.enroll hashed the display form (ABCD-1234, with the dash) while consumeRecoveryCode normalized 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 shared canonicalizeRecoveryCode helper. 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-core test failures (drifted from a permissive ServiceState transition model, the bounded-buffer EventBus, 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 = 10 in auth.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 to 0 to disable
  • Auth events on the EventBusDashboardLoginSucceeded, DashboardLoginFailed, DashboardSessionRevoked are now emitted on every login, failed login, and session revocation. The AuditCollector persists them to audit_log automatically — visible via the audit console command and GET /api/audit. Each event carries the source IP, affected UUID (when known), and a reason/scope tag
  • [console] geo_lookup_enabled = false in nimbus.toml — kill-switch for the Remote-CLI session GeoIP lookup. Default flipped to false in 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 with geo_lookup_enabled = false to disable entirely
  • Docs — [Auth Module] page now documents the API-token bypass behaviour explicitly: holding NIMBUS_API_TOKEN is implicit nimbus.dashboard.admin and 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-forwarded ModuleCommand pathway that Punishments/Perms/Scaling already use — no separate /dashboard command, 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 new AUTH_MAGIC_LINK_DELIVERY module 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-byte userHandle so 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 new dashboard_webauthn_credentials table 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 ±N step window. Secrets stored AES-GCM-encrypted with config/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-auth challengeId issued 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 fires PermissionsChanged events 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
  • 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 to true so 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_DELIVERY module events and pushes clickable chat components via TextCompat.sendClickableLink (works on Adventure-native and legacy BungeeCord-chat proxies alike)
    • Does not register its own /dashboard command — login/sessions/logout-all live under /nimbus dashboard ... via the shared ModuleCommand → Bridge /nimbus pattern, consistent with every other module's in-game surface
  • 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 existing GET /api/auth/sessions + POST /api/auth/logout-all endpoints 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 own URLClassLoader, so holding a compile-time Class<?> reference to a service registered by another module doesn't work (class identity is classloader-scoped). Existing getService<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's PermissionResolver can reach the Perms module's PermissionManager reflectively
  • ModuleCommand.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 in modules/api so modules can declare caller-scoped subcommands without depending on Velocity
  • CommandExecuteRequest carries an optional caller DTO end-to-end (Bridge → controller). CloudCommand.executeRemoteCommand populates it when the source is a Velocity Player
  • [dashboard] public_url field in nimbus.toml — canonical base URL the dashboard is reachable at. Used for magic-link construction and as the default RP origin for WebAuthn
  • NimbusModule.requires honoured during module load — the Auth module declares requires = ["perms"]. If Perms isn't installed the auth module falls back to token-only mode and logs a WARN; 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/permsmodules/perms/plugin
    • plugins/displaymodules/display/plugin
    • plugins/resourcepacksmodules/resourcepacks/plugin
    • plugins/punishments-backendmodules/punishments/plugin-backend
    • plugins/punishments-velocitymodules/punishments/plugin-velocity
    • plugins/auth-velocitymodules/auth/plugin-velocity (new in this release)

    Pure disk-layout refactor: Gradle project names (:nimbus-perms, :nimbus-auth-velocity, ...) are unchanged — only projectDir mappings in settings.gradle.kts were updated. PluginDeployment resourcePaths (plugins/nimbus-*.jar) stay the same because they reference the shadow-jar's internal resource layout, not source paths

  • TextCompat.sendClickableLink + AdventureHelper.sendClickableLink added 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 snapshot
  • dashboard_login_challenges — pending 6-digit codes + magic-link tokens, TTL-expired
  • dashboard_totp — AES-GCM-encrypted secrets + enrollment state
  • dashboard_recovery_codes — SHA-256 hashed, single-use
  • dashboard_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_url in nimbus.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/logout
  • POST /api/auth/totp-verify (public; accepts live TOTP or recovery codes)
  • GET /api/profile/totp/status, POST /api/profile/totp/enroll|confirm|disable
  • POST /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-others
  • GET /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/authauth.toml field-by-field, endpoint table, session.key backup 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_step column (migration V8005) on dashboard_totp; verifyAndAdvance atomically 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] window now coerces into 0..2 (was 0..10). Default 1 = ±30 s, hard max 2 = ±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 CredentialRepository is pre-fetch only. The earlier implementation called runBlocking { 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-memory CredentialCache before entering the library, and the in-memory repo serves lookups without doing any I/O
  • allowOriginPort is operator-gated. New [webauthn] allow_origin_port config key, off by default. Earlier iteration hard-coded true which weakened phishing-resistance (host:443 and host:3000 were 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-use wt_ 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. SessionValidator transparently 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_challenges rows instead of an in-memory map — a controller restart no longer resets the counter
  • PLAYER_OFFLINE no 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_snapshot is 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 to localStorage (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_log table via the standard AuditCollector. 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, no docker CLI 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: true so Nimbus owns a single hijacked stdio stream — stdout lines flow into the existing SharedFlow<String>, and sendCommand("stop") writes straight to the Minecraft server's stdin. No RCON, no extra ports
    • Resource limits enforced by the kernel. memory_limit maps to cgroup Memory, cpu_limit to NanoCpus (fractional cores supported). MemorySwap is pinned to Memory to disable swap thrash. Restart policy is fixed to no — Nimbus handles restart decisions, not Docker
    • Auto image selection. Picks eclipse-temurin:17-jre / eclipse-temurin:21-jre based 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. nimbus bridge 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) so server.properties / velocity.toml keep 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=true container 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 over ProcessHandle.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, ServiceMemoryResolver reads cgroup memory from docker stats instead of /proc/<pid>/status. Catches sidecar processes the plain JVM PID would miss; dashboard numbers match docker stats exactly
    • 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 doctor console command and /api/doctor
  • 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 calls POST /api/docker/prune
    • Polls via POLL.normal with visibility pausing; the list is silent on transient failures so a brief daemon hiccup doesn't spam toasts
  • Dashboard — per-group Docker toggle. GroupEditDialog grew 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 Docker badge next to each containerised service. Backed by a new backedBy: "docker" field on ServiceResponse

Core changes (minimal, non-breaking)

All additive — nothing breaks for existing installations that don't enable the module.

  • DockerServiceConfig on GroupDefinition and DedicatedDefinition (defaults to DockerServiceConfig() with enabled = false, so services without a [docker] block behave exactly as before)
  • PreparedService.dockerConfig populated by ServiceFactory from the group/dedicated config
  • LocalServiceHandleFactory interface in nimbus-core/service/ — modules register an alternative ServiceHandle factory via ModuleContext.registerService(). ServiceManager.startLocalService consults it per-start and falls back to ProcessHandle when the factory is absent or isAvailable() returns false. recover() hook on the same interface lets the module reattach to running containers on restart
  • ServiceMemorySource interface + ServiceMemoryResolver.registerSource() — pluggable memory reader so the Docker module can serve cgroup stats ahead of /proc
  • ServiceHandle.kind: String with default "process" — tagging mechanism so /api/services can surface backedBy without the core having to know about container implementations
  • GroupResponse / CreateGroupRequest carry an optional docker sub-object. buildGroupToml conditionally 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 via tcp://localhost:2375 covers 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 docker group 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.zst archives

    • Native multi-threaded archiver. In-JVM pipeline via Apache Commons Compress (TarArchiveOutputStream) + zstd-jni (ZstdOutputStream with setWorkers(N), setCloseFrameOnFlush(false)) running at 3–5× the throughput of piping to tar --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.sha256 entry, and re-checked by backup 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 INTO inside an Exposed transaction — atomic, no external tool. MySQL via mysqldump --single-transaction --routines --triggers --events, Postgres via pg_dump --format=custom — skipped with a WARN and a PARTIAL row if the tool is missing on PATH
    • 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.executeCommand plumbing — save-off + save-all flush, configurable wait, save-on after archiving. Only applies to services in READY/STARTING state on the local node; remote-node services are skipped with PARTIAL status 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 --force is 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/config validates (cron syntax, compression level 1..22, unique schedule names, allowed target keys, known retention classes) and atomically rewrites config/modules/backup/backup.toml with a deterministic layout + comments. Scheduler is hot-reloaded on success; 400 VALIDATION_FAILED leaves 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_log tables)
    • Events: BACKUP_STARTED, BACKUP_COMPLETED (with duration + size), BACKUP_FAILED, BACKUP_RESTORED, BACKUP_PRUNED
  • Dashboard — Backup page at /modules/backup with 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

Changed

  • Sidebar breadcrumbs. backup, punishments, resourcepacks, doctor, stats, and login now have proper capitalized labels instead of falling through to the raw URL segment
  • Module discovery icons. The sidebar's iconMap gained Archive and HardDrive mappings so modules with those DashboardConfig.icon values render correctly
  • Dependencies. nimbus-core now shades com.github.luben:zstd-jni:1.5.6-4 and org.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 PARTIAL status. S3/SFTP destinations, cross-snapshot dedup, and cluster streaming are tracked for follow-up work via a BackupDestination interface and a new BackupStreamRequest cluster 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

    • PunishmentManager holds 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. BedWars ban + network-wide mute)
    • Console command punish with subcommands ban / tempban / ipban / mute / tempmute / kick / warn / unban / unmute / history / list (--group / --service flags for scope)
    • In-game command /cloud punish … via the Bridge (honours the same permission nodes as the console command)
    • Mojang UUID lookuppunish 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 by POST /api/punishments for dashboard-issued punishments
    • Editable kick/mute messagesconfig/modules/punishments/messages.toml with placeholders ({target}, {issuer}, {reason}, {remaining}, {expires}). A dedicated WARN template is separate from KICK. Live-editable through the dashboard — GET/PUT /api/punishments/messages persists 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 at LoginEvent (real Mojang UUID, post-auth)
      • ConnectListener: blocks ServerPreConnectEvent for GROUP/SERVICE-scoped bans — player stays on the network, just can't enter banned backends
      • LiveKickHandler: subscribes to PUNISHMENT_ISSUED on the event stream so bans take effect instantly for already-connected players
      • PunishmentsApiClient: 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 to AsyncPlayerChatEvent and 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_target columns; pre-existing rows default to NETWORK)
    • Events: PUNISHMENT_ISSUED (carries a pre-rendered kickMessage), 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/resourcepacks with SHA-1) or locally-hosted packs (POST /api/resourcepacks/upload — raw-body streaming upload, SHA-1 computed during write, atomic rename with fd.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 during setResourcePack() protects against tampering. Response streams through Ktor's respondOutputStream with a 64 KiB transfer buffer so 250 MB packs don't spike server RAM
    • Telemetry — backend plugin reports PlayerResourcePackStatusEvent results to POST /api/resourcepacks/status, surfaced as the RESOURCE_PACK_STATUS event 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
  • Module API — PluginTarget for proxy pluginsPluginDeployment gained a target: PluginTarget field (BACKEND default, VELOCITY for proxies). Modules can now register plugins for proxies too, not just backends (ServiceFactory.resolveModulePlugins iterates per-target on every service prepare)

  • Mojang profile API lookup helperMojangUuidLookup resolves a username to a canonical (uuid, name) pair via api.mojang.com, used by the Punishments REST API and the punish console 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-ownedtemplates/global/plugins/ and templates/global_proxy/plugins/ no longer receive nimbus-sdk.jar, nimbus-bridge.jar, or any module-provided plugin. All of them are now deployed at runtime on every service prepare via ServiceFactory.resolveModulePlugins(), using REPLACE_EXISTING so deleted/modified files self-heal on next start. Only bridge.json config and Bedrock (Geyser/Floodgate) plugins remain written into templates. Migration for existing installs: delete the Nimbus-managed JARs from your templates/global/plugins/ and templates/global_proxy/plugins/ directories (anything matching nimbus-*.jar); they will be re-deployed on the next service start. If you leave them in place they'll simply be overwritten
  • modules console command no longer offers to deploy plugins into templates — the old offerPluginDeploy path has been removed. Module plugins are runtime-deployed per service
  • ServiceFactory.prepare now reuses the lowest-numbered CRASHED/STOPPED slot instead of advancing to fresh numbers, so a crashed Lobby-1 stays Lobby-1 across 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/icons backed by lucide-react. Smaller bundle, consistent stroke weights

Fixed

  • Signed-chat disconnect on mute — cancelling PlayerChatEvent from a Velocity EventTask.async handler 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 new punishments-backend plugin, which cancels AsyncPlayerChatEvent synchronously — 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 populatedChatListener pre-fetch and LiveKickHandler refresh both called server.getScheduler().buildTask(new Object(), …). Velocity's scheduler requires a registered @Plugin as task owner — passing a plain Object makes the call throw, the task never runs, the cache stays empty, and the sync chat handler falls through to allow. Swapped both call sites to CompletableFuture.runAsync
  • Punishments by username never matched real players — dashboard-issued punishments fell back to 00000000-… when targetUuid was omitted. POST /api/punishments now accepts explicit UUIDs, UUID-shaped strings in the targetName field, or plain usernames (resolved via MojangUuidLookup off the IO dispatcher). Unknown names return 404 with 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 expects application/octet-stream. Client switched to the shared apiUpload() helper (correct key + HTTPS-proxy fallback); route uses call.receiveStream() to match the ModpackRoutes pattern — 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, 0 to disable)
  • Configurable metrics retention — New [metrics] retention_days config (default 30). Previously hardcoded
  • Release checksum verification — GitHub release workflow now generates and uploads SHA256SUMS.txt alongside JAR artifacts, enabling install.sh integrity verification
  • Log archive retentionLogRotation now prunes old .log.gz archives, 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) to DROP_OLDEST to prevent cascading backpressure under extreme event load
  • Group reload is rollback-safePOST /api/reload now 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 DatabaseInitException that propagates naturally, allowing shutdown hooks to run and resources to clean up
  • MySQL migration lock timeout ignoredGET_LOCK return value is now checked; throws MigrationException if 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}/send and /api/players/{name}/kick now 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 DELETE with 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 updatesmerge() 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" logClusterStarted event 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 lockMigrationManager acquires 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_for config 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_interval config (default 10s) for game modes needing faster scaling reaction
  • Install script restart on exit code 10install.sh and install.ps1 detect exit code 10 and re-launch automatically, enabling zero-downtime auto-updates
  • Install script SHA-256 verificationinstall.sh verifies JAR checksum from release assets when available
  • Human-readable uptimeGET /api/status includes uptimeHuman field (e.g., "2h 15m 30s")
  • Bridge JAR version-stamped — Embedded JAR renamed to nimbus-bridge-<version>.jar for 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_origins changed 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_size to 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 sessionsrunBlocking inside 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 awaitServicesReady prevents missed READY events during 20+ simultaneous service starts
  • EADDRINUSE on rapid restartsrestartService now 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 ServiceCrashed events alongside ServiceStopped
  • Scale-down blocked by pending services — Pending services only block scale-up evaluation, not scale-down
  • Module disable crash takes down shutdowndisableAll() 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 shutdownstopAll() 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 /tmp and data dir are on different volumes
  • WarmPool not cleaned on group delete — PREPARING slots released when a group is disabled
  • Bootstrap URL double portcluster bootstrap-url no longer appends redundant port when public_host already 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 unauthenticatedPOST /api/console/complete now 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 eventsPOST /api/proxy/events validates 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.properties when 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-owner markers. On recovery, orphaned processes are detected and killed based on these markers instead of unreliable ProcessHandle.commandLine()
  • Reconnect reconciliation — Agents send their running service list on every reconnect. Controller purges ghost entries the agent doesn't claim
  • POST /api/shutdown endpoint — 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 orderingShutdownAgent sent 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 splitGET /api/stress status 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 = true on 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/trigger endpoint
  • TRIGGER_SYNC cluster 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 triggerPOST /api/services/{name}/sync/trigger endpoint 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 | fail controls offline behavior
  • Dynamic state sync[group.sync] enabled = true stores 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-1 stays Lobby-1 across crash-respawn cycles
  • Dedicated services on agent nodes[dedicated.placement] with mandatory state sync
  • extra_sans and public_host cluster 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, and SheetBody components across all pages
  • Historical memory chartsservice_metric_samples table (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
  • dedicated console command — List, create, start, stop, restart, delete, info with interactive creation wizard
  • Dashboard dedicated page — Create, configure, modpack import, and management actions
  • Controller info APIGET /api/controller/info returns version, uptime, memory budget, and update status
  • Unified memory reading — RSS read from /proc/<pid>/status (Linux/WSL) or tasklist (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 simplifiedreportHealth() 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_memory from 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/Forge mods.toml, Fabric fabric.mod.json, and legacy mcmod.info
  • Protocol version filtering — Client protocol version must match the group's MC version
  • Connection type compatibilityLEGACY_FORGE matches Forge only; MODERN_FORGE matches Forge or NeoForge
  • Alternate group fallback on kick — Modded clients try a different modded group before disconnecting

Changed

  • GET /api/groups now includes modIds field

[0.5.2] - 2026-04-09

Auto-configure Velocity for large modpacks.

Fixed

  • Silent disconnects with large modpacks — Auto-sets -Dvelocity.max-known-packs=512 when 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 key errors
  • Modded client detection timing — Uses Velocity's ConnectionType (set during FML handshake) instead of player.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-forge placement — 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() to enable(), 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_class in module.properties and load via Class.forName(), bypassing unreliable ServiceLoader
  • Version-incompatible modules still loaded — Skip logic now correctly removes modules before loading
  • Silent Error-class crashescatch (Throwable) instead of catch (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 LinuxFiles.walk() now follows symlinks so libraries/ is found through symlinked directories
  • Module version skip logic — Skipped modules properly removed before ServiceLoader runs
  • Module error handlingcatch (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_origins in generated config
  • Unknown config keys crash — TOML parser now tolerates unknown keys for forward compatibility
  • Wrong key name in docs — Dashboard guide referenced cors_origins instead of allowed_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_key in 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:slug prefix, and .zip files
  • Memory input normalization — Bare numbers auto-suffixed (e.g., 88G, 512512M)

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 APIPOST /api/modpacks/import, GET /api/modpacks/platforms
  • Plugin management APIGET /api/plugins/search, POST /api/plugins/install, GET /api/plugins/installed/{group}
  • Software versions APIGET /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 endpointsPOST /api/console/complete for tab completion, WS /api/console/stream for 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
  • sessions commandsessions active and sessions history to monitor Remote CLI connections
  • CLI connection events — Live events in local console showing who connected, from where, with OS details
  • Install scriptsinstall-cli.sh and install-cli.ps1 for one-command CLI installation

Changed

  • All console commands refactored to CommandOutput interface for remote execution support

[0.3.0] - 2026-04-07

Competitive features: closing the gaps with SimpleCloud and CloudNET.

Added

  • Template stackingtemplates = ["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_stop and lifecycle.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 feedbackstart, stop, restart report actual success/failure with details
  • Silent memory parsing — Invalid cluster node memory formats now logged instead of silently returning 0

Changed

  • Config validation on startupcontroller.max_memory, bedrock.base_port, and database.port validated 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_log table. Actor tracking, console command, REST endpoint, configurable retention (default 90 days)
  • Environment variable overridesNIMBUS_API_TOKEN, NIMBUS_DB_*, NIMBUS_CLUSTER_TOKEN, NIMBUS_AGENT_TOKEN, NIMBUS_AGENT_CONTROLLER override TOML config
  • Cluster TLS — Auto-generated self-signed keystore, wss:// default. Configurable trust via tls_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 renameKryonixMC to NimbusPowered, package dev.nimbuspowered.nimbus
  • Cluster default protocolwss:// instead of ws:// (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
  • health command — Overview of service health across all groups
  • /api/services/health endpoint — 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

  • version command — 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
  • modules command — Install, uninstall, list, info
  • plugins command — Live search across Hangar + Modrinth with version matching and dependency resolution
  • kick / broadcast commands
  • Least-players lobby balancing

Changed

  • Package rename from dev.nimbus to dev.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

On this page

[1.0.0] - 2026-04-24
Changed
Notes
[0.14.0] - 2026-04-24
Removed
Migration
[0.13.0] - 2026-04-21
Added
Changed
Docs
Migration
[0.12.0] - 2026-04-20
Added
Changed
Fixed
Security
Known follow-ups (deferred)
Notes
[0.11.2] - 2026-04-19
Added
Changed
Fixed
Notes
[0.11.1] - 2026-04-19
Added
Changed
Known follow-ups (deferred)
[0.11.0] - 2026-04-19
Added
Core changes (minimal, non-breaking)
Refactors
Database
Configuration
API
Docs
Security hardening (pre-release review)
Notes
[0.10.0] - 2026-04-17
Added
Core changes (minimal, non-breaking)
Notes
[0.9.1] - 2026-04-15
Added
Changed
Notes
[0.9.0] - 2026-04-14
Added
Changed
Fixed
[0.8.2] - 2026-04-13
Added
Changed
Fixed
[0.8.1] - 2026-04-13
Fixed
[0.8.0] - 2026-04-12
Added
Changed
Fixed
Security
Compatibility
[0.7.3] - 2026-04-12
Added
Changed
Fixed
[0.7.2] - 2026-04-11
Changed
Removed
Fixed
[0.7.1] - 2026-04-11
Added
Fixed
[0.7.0] - 2026-04-11
Added
Changed
Fixed
[0.6.2] - 2026-04-10
Added
Changed
Fixed
Removed
[0.6.1] - 2026-04-10
Added
Changed
Fixed
[0.6.0] - 2026-04-10
Added
Changed
Fixed
[0.5.3] - 2026-04-09
Added
Changed
[0.5.2] - 2026-04-09
Fixed
[0.5.1] - 2026-04-09
Fixed
Changed
[0.5.0] - 2026-04-09
Added
Fixed
[0.4.7] - 2026-04-09
Fixed
[0.4.6] - 2026-04-09
Fixed
[0.4.5] - 2026-04-09
[0.4.4] - 2026-04-09
Fixed
[0.4.3] - 2026-04-09
Added
Changed
[0.4.2] - 2026-04-09
Fixed
[0.4.1] - 2026-04-08
Added
Changed
Fixed
[0.4.0] - 2026-04-08
Added
[0.3.1] - 2026-04-07
Added
Changed
[0.3.0] - 2026-04-07
Added
[0.2.1] - 2026-04-07
Fixed
Changed
[0.2.0] - 2026-04-07
Added
Changed
Fixed
[0.1.4] - 2026-04-05
Added
Changed
[0.1.3] - 2026-04-05
Added
Changed
[0.1.2] - 2026-04-05
Added
Changed
[0.1.1] - 2026-04-04
Added
Changed
Fixed
Security
[0.1.0] - 2026-04-01
Core
Server Software
Networking
API & Console
Infrastructure
Plugins