- Version:
v0.3.0(pre-v1), released 2026-10-05. See the changelog. - Status as of 2026-10-14: The CRDs, operator, API, agent, and dashboard are feature-complete for the v1 scope and stabilized for external testing, but the project is not yet recommended for unattended production. This page tracks what stands between v0.3 and a v1 GA release.
- What works today: see
README.mdandarchitecture.md.
It is a living document: items move out of it as they ship, and anything discovered along the way gets added.
- Independent central panel: Docker Compose runs the API and dashboard without a host Kubernetes cluster; a standalone Helm profile runs without the central operator or workloads (changelog).
- Credential maintenance: standalone key custody, offline
rotate-panel-key,PUT /clusters/{name}/kubeconfig, andPUT/DELETE /clusters/{name}/gateway(changelog). - Extended telemetry tier: a second, separately consented tier with a random install ID and fixed-set categories; signed reports (changelog).
- Telemetry receiver dashboard and an optional public
GET /v1/summary(changelog). - Proof-of-work on telemetry ingestion, off by default (changelog).
- Telemetry provider runbook at
docs/telemetry-provider.md(changelog). - Optional Steam display names on player lists, via a Steam Web API key (changelog).
- Nuclear Option player lists are structured (
steamId,faction) (changelog). - New optional
GameTemplatefieldspec.rcon.authentication(defaulttrue) allows consoles with no RCON Secret (changelog). - Standalone credential protection: keys kept outside database volumes, transactional rotation (changelog).
- Remote workload boundaries: verified TLS, dial-time unsafe-destination rejection, bounded responses;
GAMEPLANE_REMOTE_ALLOWED_CIDRS/api.remoteAllowedCIDRs(changelog). - Removed: the Hell Let Loose module is withdrawn from the official catalog (changelog).
- Upgrade-note behaviour changes: telemetry is on by default for new installs after a first-login notice; installs with a saved basic toggle start sending to the default receiver after upgrade; an empty
api.telemetry.endpointmeans the project default receiver; the receiver/metricsmoved to:8081and needs the dashboard token; the bundled receiver stores data on a PVC by default; receiverDASHBOARD_TOKENmust be at least 32 characters (changelog). - Breaking: selected-cluster module and template grants need a cluster-wide grant for remote catalogs (changelog).
- Feature catalog: FEATURES.md lists every capability with its maturity and default, mirrored on the website and kept in step by
make check-features-sync(spec 024, PR #703).
- Configurable backup/restore Job retry limit and deadline Helm values (changelog).
- Multicluster gateways and unified server management across clusters (changelog).
- User theme customization: presets, custom colors with a WCAG AA check, custom CSS overlay, safe mode (changelog).
- Network capture sidecar: admin-only, on-demand PCAPNG capture from running game pods (changelog).
- Module authoring toolkit
gp-module(scaffold, validate, preview, package) and the dashboard Modules builder (changelog). - 13 new top Steam game modules (changelog).
- Nuclear Option dedicated server support (changelog).
- Install-time config: Helm-seeded OIDC role mappings and a default StorageClass for game-data PVCs (changelog).
- Share link expiry options (presets, custom date, no expiry) replacing the 90-day cap (changelog).
- Console WebSocket robustness on non-default ports, and input queueing (changelog).
- Existing PVC for SQLite via
api.storage.existingClaim(changelog). - REST and CLI console protocols for games with HTTP admin APIs or PTY-only consoles (changelog).
- Mod archive path confinement (zip-slip and symlink guards) (changelog).
- Design and visual polish: HeroUI foundation and brand re-theme (changelog).
- Experimental PostgreSQL backend works end to end (changelog).
- Owner-only server operations (transfer, collaborators, wipe, delete) need the owner or an admin (changelog).
- Data wipe failures fail the Job and set
DataWipe=False; backup/restore quiesce lifecycle fixed, and restore removes files created after the snapshot (changelog). - SQLite DSN gets
busy_timeout(5000)by default; agent metrics on a separate unauthenticated:9090listener that the PodMonitor scrapes (changelog). - Multi-cluster request scoping, OIDC role re-evaluation, bootstrap-admin reset, cluster registration removal, mod bundle integrity, and tunnel credential handling hardened (changelog).
- Upgrade note: every game pod gains an empty capture
emptyDir, so existing GameServers roll once on upgrade (changelog).
The initial dashboard selector routed requests to one cluster at a time. The unified dashboard now combines authorized resources with optional location filters; selection remains in node inventory administration. Server operations retain their own cluster and namespace. Pod logs and PTY WebSockets use that cluster's Kubernetes API.
The optional private agent gateway supports RCON, game-file logs, file/player operations, module actions and agent-based mods. Remote capture downloads and cleanup bind files to GameServer and NetworkCapture UIDs. Registry browsing, modpacks and ID-list configuration use the selected Kubernetes client and template. Upgrade the gateway, operator, agents and capture sidecars together; direct Kubernetes connectivity remains required.
Dedicated gateway health in the dashboard remains follow-up work. The existing cluster-health indicator reports Kubernetes connectivity only.
A real kind ×2 e2e test (multicluster bucket) asserting ?cluster= dispatch
lands a GameServer on cluster B and that a viewer scoped to cluster A cannot see it.
Complete end-to-end. Backend: migration 005_audit_chain.sql,
Auditor.insertChained/Auditor.Verify, and GET /admin/audit/verify. The
dashboard integrity banner — surfacing a verification break to the admin on the
audit page — shipped in PR #108.
Design (as implemented):
- Migration
005_audit_chain.sqladdedprev_hash+hashcolumns. - On insert (inside a transaction, serialized per-process by a mutex — see the
single-writer caveat on
insertChained) computehash = SHA-256(prev_hash || canonical(row)), and upsertaudit.head(the newest row's id + hash) in the same transaction. Verify()walks the chain and reports the first break, exposed as an admin-onlyGET /admin/audit/verify. It also checksaudit.headagainst the newest surviving row, so truncating the tail (DELETE ... WHERE id > N, which leaves every surviving link internally consistent) is detected too — previously a known blind spot.Prunewrites a checkpoint (the newest about-to-be-deleted row's id + hash, in theconfigtable underaudit.checkpoint) before deleting, so verification resumes from the checkpoint rather than the genesis row.
What this does and does not guarantee: the chain is unkeyed, and the
checkpoint/head it verifies against live in the same DB-writable config
table. It reliably catches naive in-DB tampering (UPDATE/DELETE, including
tail truncation) and accidental corruption — it does not, and cannot, defeat a
sophisticated attacker with DB write access who also recomputes and rewrites
the checkpoint/head to match. The external sinks (stdout/webhook/S3) remain the
append-only record of last resort such an attacker cannot retroactively alter.
An HMAC-keyed chain (key held outside the database) would close that
remaining gap and is a candidate future hardening, not yet implemented.
Threat-model context lives in security.md.
helm upgrade was the most dangerous operation a user performs and the only
one CI never exercised. It now runs on every PR, on both amd64 and arm64:
deploy/kind/upgrade.sh installs the previous release's published chart and
published GHCR images, TestUpgrade_FromPreviousRelease seeds a running
GameServer with data plus an admin user, upgrades to the working-tree chart,
and asserts the CRD schema was updated, the server and its volume bytes
survived, and the pre-upgrade admin can still log in (proving migrations ran
against a populated SQLite volume).
The CRD assertion is self-calibrating: it computes which schema properties the working tree declares that the installed release lacks, then requires the pre-upgrade hook to have added exactly those — so it cannot rot into a vacuous pass as the schema evolves.
This closes the CRD-upgrade caveat as a tested path rather than a documented
one. Note the caveat's own docs were stale and are corrected: the chart's
crds.autoApply pre-upgrade hook has been applying CRDs automatically for a
while, but install.md still told users to run
kubectl apply by hand.
Not yet covered: upgrades skipping several releases at once, and Postgres [experimental].
Opt-in per server (spec.idle): the operator scales a GameServer to zero once
it has reported no online players for afterMinutes, and brings it back on a
wakeWindows cron tick or an explicit POST /servers/{name}:wake. On a
single-node homelab the overnight reservation of several idle servers is the
dominant cost; this releases it without touching the data volume.
Design (as implemented):
- Sleeping drives the existing
softStoppath, so the module-declared stop sequence runs and the world saves before the pod goes away. - The sleep marker is an operator-owned annotation, not
spec.suspend. That field is the user's own power switch (the:start/:stopverbs patch it), so overloading it would make an automatic sleep indistinguishable from a deliberate stop — and a wake window would then resurrect a server its owner had turned off. Wake uses a request/ack token pair like the restart primitive. - Phase reuses
Suspended; theIdleAsleepcondition reason,status.idle, and an-o wideprint column distinguish it from a manual stop. - A stale heartbeat or an absent player count both freeze the idle clock rather
than reading as empty — unknown is not zero. Each refusal records a reason on
status.idle, so a server that can never sleep explains itself. gameplane_gameservers_idle{state=asleep|awake}reports the saving.- The dashboard (PR #182) renders a slept server as Asleep rather than
Suspended, and replaces its Start button with Wake. That substitution is a bug fix, not decoration: an asleep server hasspec.suspendstillfalse, so the Start button the phase used to offer patched a field to the value it already held — a silent no-op that never woke anything. Only:wakeclears the operator's marker. - The server's Overview surfaces
status.idle.reason(capitalized) as the sleep card's own state line — including while still asleep, if a wake window failed to parse — so a server that isn't accumulating idle time says why instead of looking broken. The one reason meaning the trigger can never fire ("this game reports no player count") gets a plainer "Will never sleep" label instead, with the operator's reason underneath it.
Follow-up: status.idle.nextWakeTime. The dashboard lists the configured
wake windows as raw cron strings, because the operator computes the next tick
only for its own requeue and never persists it — and re-deriving it in the
browser would duplicate scheduling semantics the operator owns (rule 10).
Persisting it on status is the right fix, and is a CRD change rather than a
dashboard one.
A sentinel [optional] component holds advertised ports while a server is asleep and wakes
it on a genuine connection attempt. Opt-in per server via spec.idle.wakeOnConnect
(default false), with per-port protocol awareness to avoid corrupting handshakes.
Design (as implemented):
- New optional component:
sentinel/(distroless Docker image, configured viaoperator.sentinelImage). While a GameServer is armed (spec.idle.wakeOnConnect=true) and asleep, the operator runs it as a 1-replica Deployment<gs>-waker. - The sentinel holds the game's advertised ports (read from
spec.networkingandGamePort.advertise) and switches traffic between the game pod and itself via Service endpoint surgery: when asleep, the Service points to the sentinel; when waking, it points back to the game pod (via a<gs>-game-directClusterIP Service the game pod advertises to). - New shared module:
gameproto/for Minecraft and Terraria real handshake parsing (Consumedfield lets callers reconstruct the client stream for lossless replay). Every other game usesgeneric— a packets-in-window heuristic. - New CRD fields:
GameServer.spec.idle.wakeOnConnect(bool, default false) andGamePort.wakeProtocol(enum:minecraft|terraria|generic|none, defaultgeneric). - Modules:
minecraft-javaandterrariadeclare their parser. - Server-list pings are answered in place without waking (parsing distinguishes a join from a status query); the player's client gets a protocol-native "waking up, try again" message if the game misses the deadline (25s default).
- UDP-only games (Valheim, Factorio, etc.) hold no connection, so they use the generic heuristic and wake-and-drop — the client retries.
- Hostport mode is deliberately asymmetric: the sentinel cannot hold the host port the game pod needs to schedule, so on wake it bounces held clients and exits. Service-backed modes (ClusterIP/NodePort/LoadBalancer) proxy through until Ready.
- Costs one small pod per sleeping server, which is why it is opt-in.
Honest limitations: Only Minecraft and Terraria get real parsing; tModLoader
also declares the Terraria parser. The other 26 of the 29 catalog modules use the
generic default (they hold no connection to parse), and hostport is asymmetric as described.
An MCP server letting an AI assistant read current cluster state and propose fixes — strictly read-only, no writes.
A new optional component [optional] (distroless Docker image, Helm toggle) exposing only
List / Get / Watch over the Gameplane CRDs plus Pods, Events, and pod logs.
"Propose a fix" returns suggested YAML or kubectl invocations as text — no
create/update/delete/patch tool exists. See mcp-server/README.md.
The keyed-cosign signing mechanism is implemented and e2e-proven, and
ModuleSource.spec.verify can require a valid signature. It is now active
for official bundles with Sigstore Rekor transparency logging: cosign.pub
(ECDSA P-256) is committed under signing/ (and baked into the chart for
module verification), and the Helm chart is signed for the first time.
publish-edge signs the rolling images, release signs the tagged images,
the chart, and the official bundles, and republish-modules re-signs bundles
out of band — all by digest with the provisioned COSIGN_PRIVATE_KEY, all
recording their signatures in the public Rekor transparency log, and all
fail-closed when the key is absent. The operator's verify path remains
offline/keyed, preserving air-gapped functionality. See
Signing official bundles.
Every shipped module's default image is now pinned by digest, so a server
binary can no longer change underneath a user on pod restart with no version
bump or changelog. This was not hypothetical: terraria-latest moved from
Terraria 1.4.4.9 to 1.4.5.6, changing the network protocol version and
breaking the e2e Terraria bot.
Eight games also gained a real spec.versions list, so a floating tag now
survives only where drift is the user's explicit, labelled choice. The
module repo's validate.py enforces the rule (spec.image and any
default: true entry are an error if unpinned, with a # gameplane:floating
opt-out), and a monthly workflow refreshes the pins. Authoring rules live in
Pinning images by digest.
Status as of 2026-10-14. Task counts come from each spec's tasks.md.
- Spec 002, Nuclear Option module and load-balancer IP pool override (
specs/002-nuclear-option-ip-pool/): 136 of 143 tasks done. Blocked: the server does not bind UDP 7777, so no join client can be built until a real-client packet capture documents the handshake (maintainer decision, 2026-10-11). Unblocked by: that packet capture. The join test, its bucket and the coverage row wait on it. - Spec 009, code scanning remediation and Dependabot (
specs/009-remediate-security-dependabot/): 80 of 82 tasks done. Blocked: TypeScript 7 is blocked because the@typescript-eslintpeer range stops before TypeScript 7, and ESLint 10 is blocked becauseeslint-plugin-reactdoes not accept^10. Unblocked by: a@typescript-eslintrelease that accepts TypeScript 7 (then T055/T056 and PR #272), and aneslint-plugin-reactrelease that accepts^10(then PR #386, and #387 if open). - Spec 013, expand test coverage (
specs/013-expand-test-coverage/): 39 of 62 tasks done. No external blocker is named. Open work is the live web e2e specs, the optional-components e2e, the Postgres e2e, docs and checks, and the archive step. Some tasks wait for the first green CI run of their job, and the archive waits until every PR is merged. Open PRs for this spec are in progress. - Spec 019, app localization and community language packs (
specs/019-localization-language-packs/): 0 of 105 tasks checked; the four design decisions are settled. The implementation is on open PR #668 and is not yet onmaster. Unblocked by: merging PR #668, which needs human approval. The first step is thei18n/module and checker. - Spec 021, edit game configuration from server Settings (
specs/021-settings-game-config/): implemented, 20 of 24 tasks done, pending CI and review. Blocked: CI must go green and a human must approve the PR. Unblocked by: that approval and merge, then archiving todone_021. - Spec 022, default telemetry destination, extended telemetry and telemetry dashboard (
specs/022-default-telemetry-dashboard/): 109 of 119 tasks done. The shipped feature is unchanged until the version-trend amendment (2026-10-14, T101 to T110) lands. Blocked: T104 needs Pencil design work on the machine where Pencil runs; T109 needs the user's sign-off before editing existing testslifecycle_test.go:11andcharts_test.go:83; T110 waits for the PR to merge. Unblocked by: the design work, that sign-off, and the merge. - Spec 023, change-based test selection (
specs/023-change-based-test-selection/): 0 of 52 tasks done; no open decisions remain. No blocker is named. T001 creates theci: full-runlabel. T052 needs 30 merged PRs to measure runner minutes. - Spec 025, standalone panel and API (
specs/025-standalone-panel/, see standalone panel): implemented and merged in PR #630; all 51 tasks done, with known gaps listed in the spec. The folder was renumbered from023-standalone-paneland written after the fact. Next step: archive todone_025.
Nothing hard-blocks v1 anymore. What stands between v0.3 and a v1 GA is the production-readiness hardening below — tracked items, not code gaps.
- A documented backup/restore drill — a runbook an operator can follow.
The coverage half of this item is already done and was stale here:
TestRestore_RoundTrip(test/e2e/restore_e2e_test.go) writes a marker into a server's PVC, backs it up, wipes it, restores, and asserts the bytes came back — against the real in-cluster restic-server thatensureResticRepoprovisions. What is missing is the human-facing runbook, not the test. - Resource-limit guidance sized from real workloads rather than defaults.
SQLite is the only production-tested driver. The Postgres driver (build tag
-tags postgres) now works end to end (#518): migrations 001–012 have
hand-written Postgres equivalents (api/internal/db/migrations/postgres/),
every later migration is one portable file both drivers run
(migrations/common/), the Postgres connection rewrites ? placeholders to
$n, and runtime timestamps are generated in Go. The api (postgres) CI job
builds the api with the tag and runs api/internal/db's tests against a real
PostgreSQL server. It stays experimental until the rest lands: running the
handler/auth/audit test suites and the kind e2e and upgrade suites against
Postgres, a published image built with -tags postgres, and multi-replica
safety (the user-management lock and the audit hash chain assume a single API
process).
The Users page has two placeholder tabs (web/src/routes/Users.tsx,
ServiceAccountsTab and IdpTab). Service accounts (machine-to-machine
API tokens) don't exist yet and are tracked for v1.1. Identity providers
has no controls of its own: OIDC providers are already managed under Admin
Settings → Authentication (see OIDC provider setup), or through
the api.oidc.* Helm values. The tab's own UI is tracked for v1.1.
Constitution IV requires a specs.md file per game module documenting the
template's authoring and configuration — the game module itself, not its
join-protocol wire format. Those specification files belong in the
gameplane-module submodule repo (e.g., modules/minecraft-java/specs.md),
are tracked there in the module repository rather than in this main repo, and
are a known incomplete item for v1. As of 2026-10-14, 26 of the 29 modules have
a specs.md; enshrouded, minecraft-java and v-rising do not yet. See the
module submodule's project board for status.
- A hosted/managed Gameplane (not planned). The project targets self-hosted clusters, from a single-node k3s homelab to multi-node production.
Every Gameplane-shipped game module now has a recorded, verified coverage
status for its join-protocol E2E testing. See docs/game-coverage.md for the
canonical per-module status:
- 2 modules have real join coverage in CI (
minecraft-java,terraria). - 25 modules are blocked on undocumented or partially-documented wire-protocol formats, each with a named unblocking artifact (packet capture, reverse-engineering session, or anti-cheat analysis). They are candidates for future protocol work once documentation becomes available.
- 2 modules are permanently out-of-scope-by-design due to architectural
constraints (
valheimroutes through Steam Datagram Relay;dayzrequires BattlEye anti-cheat validation before join completion).
Total: 29 modules.