Skip to main content

Games

The Games screen (route /games) is where you declare what servers exist. It is the app's editor for gameServers in the versioned JSON configuration object (deployment-config.json, in your S3 configuration bucket) — there is no separate variables file any more.

The rule that governs this whole screen

Creating, editing or removing a game only updates the JSON configuration object. Nothing changes in AWS until a plan/apply run.

Adding a game does not create a task definition. Removing one does not delete anything — the deployed task definition, EFS access point and security-group rules stay live. Editing CPU or ports does not touch the running server.

Every one of those changes is inert until you go to Infrastructure and run plan → approve → apply.

The Games screen showing a table of declared game servers with name, status chip, image, ports, CPU and memory columns, and an Add game button

The games table

The card is titled Declared game servers. One row per game, listing everything declared in the configuration object first, then anything deployed but no longer declared.

ColumnContents
NameThe game name, linked to its detail page
StatusOne chip — see below
ImageThe container image, or
Ports25565/tcp, 25575/tcp, or
CPUFargate CPU units, or
MemoryMiB, or

The table has no sorting and no filter box. Only the name cell is clickable — clicking elsewhere in the row does nothing.

The page fetches once when you open it. Navigate away and back to refresh it.

Status chips

Exactly one chip per game.

ChipMeaningCause
In sync (green)Declared in the configuration object and present in the last-applied Pulumi stack outputs, with matching configNormal, healthy
Config drift (amber)Declared and applied, but one or more fields (image, CPU, memory, ports, volumes) differ between the declared config and what was last appliedYou edited the game after the last apply
Pending deploy (amber)Declared in the configuration object, not yet in the applied outputsYou added or changed it but have not applied yet
Undeclared (red)In the applied outputs, but no longer in the configuration objectYou removed it from the configuration object but have not applied the removal

"Undeclared" is the one that surprises people. It means the game is still live in AWS — its task definition, EFS access point and log group all still exist and can still be started — but there is no declaration for it any more. The usual cause is having pressed Remove game without running a plan/apply afterwards.

"Config drift" compares against the last-applied Pulumi stack snapshot, not live AWS state. It flags "this field was edited since the last apply", not "someone changed the running task out-of-band from the AWS console" — the app never reads live ECS/EFS state for this comparison. Hover the chip's tooltip to see which fields changed; running a plan/apply from Infrastructure resolves it the same way it resolves Pending deploy.

Because there is no declaration, an undeclared row has no config to show: its Image, Ports, CPU and Memory columns are all . Applying a plan is what actually tears it down.

The pending-changes banner sits above the table and summarises the same drift in counts.

Adding a game

Add game opens a six-step dialog titled Add a game server, with the current step shown as Step 1 of 6: Identity.

The Add game wizard on its Identity step, with Name, Image and Connect message fields and Back/Next buttons

Step 1 — Identity

FieldExampleRules
NameminecraftRequired. Must start with a letter or underscore and contain only letters, numbers, underscores and hyphens. Must not duplicate an existing declared game
Imageitzg/minecraft-serverRequired
Connect messageConnect at {ip}:25565Optional. Only the placeholders {host}, {ip}, {port} and {game} are allowed

The name becomes the gameServers map key, the task-definition family ({name}-server), the log group (/ecs/{name}-server) and the DNS label — which is why it is validated as an identifier and why it cannot be changed later.

Step 2 — Resources

Two sliders, no free text: vCPU and Memory. Each slider snaps to the valid Fargate tiers below rather than any free value, so the memory slider is disabled until you pick a vCPU and is then rebuilt from that choice. Changing the vCPU after the fact resets the memory if the current value is no longer valid for the new tier.

CPU unitsValid memory (MiB)
256512, 1024, 2048
5121024 – 4096, in steps of 1024
10242048 – 8192, in steps of 1024
20484096 – 16384, in steps of 1024
40968192 – 30720, in steps of 1024
819216384 – 61440, in steps of 4096
1638432768 – 122880, in steps of 8192

1024 CPU units is 1 vCPU. Because the sliders are generated from this table, the wizard cannot offer you an invalid pairing. A live estimated hourly cost ($X.XXXX/hr while running) appears below the sliders and recomputes as either slider moves, so you can see the cost impact of a resource change before submitting.

Step 3 — Networking

Declare every container port the server listens on.

Starts empty. Add port appends a row with a Container port number field, a Protocol dropdown (TCP / UDP, defaulting to TCP), and a Visibility dropdown (Public / VPC-only, defaulting to Public). Each row has its own Remove button.

Public ports are open to the whole internet, same as before this control existed. A port set to VPC-only is reachable only from inside the VPC — useful for a management/REST port a game exposes for the health check below, so the internet can't reach it but an in-VPC component (like the authoritative health check) still can. Note that this is VPC-wide reachability, not scoped to any one caller — see Infra program reference for the security-group detail.

Two collision checks run continuously:

  • Against the other ports in this same game: Port 25565/tcp collides with ports[0] in the same game server.
  • Against every other declared game: Port 25565/tcp collides with existing game "minecraft".

Both are hard blocks. Note that zero ports is technically allowed by the wizard — you can advance without adding any — but a server with no declared ports will not be reachable.

Below the port rows, an Enable HTTPS (Caddy sidecar) checkbox sets the game's https flag. Checking it shows a warning: enabling HTTPS opens ports 443 and 80 to the internet for the whole stack, not just this game, and this game's raw container port loses its public ingress rule — traffic reaches it through the sidecar instead. Two validations key off this flag: an https = true game must declare at least one port (An https = true game server must declare at least one port.), and its first port must be tcp (The first port entry of an https = true game server must use protocol "tcp" (exact, lowercase).).

Health check (optional)

Below the HTTPS toggle, an Enable authoritative health check checkbox lets you judge this server active/idle by an HTTP request instead of network traffic — for a game whose management API can answer the question directly (Palworld's REST API is the reference case), rather than relying on the network-packet heuristic every other game uses. Leaving it off keeps that default heuristic unchanged.

Checking it exposes:

FieldPurpose
Schemehttp or https
PortA dropdown constrained to the ports declared above — you cannot target a port this game doesn't expose
MethodGET, POST, PUT, PATCH, or HEAD
Timeout (ms)100–10000. Bounds the entire request as one wall-clock budget
Request pathRooted at /, e.g. /status
Response JSON pathA field path into the JSON response body, e.g. players.online (plain field access and numeric array indices only — no wildcards)
Operatorequals, notEquals, greaterThan, lessThan, contains, or exists. exists hides the comparison-value field entirely — it only tests whether the path resolves to anything
Comparison valueCompared against the resolved value; hidden when the operator is exists
Credential typeNone / Raw ARN / Basic / Bearer — see below

Two validations key off this block: a declared port not among this game's own ports (healthCheck.port <N> is not among this game server's declared ports.), and a comparison operator declared without a value (healthCheck.activeWhen.value is required for operator "<op>"; only "exists" takes none.).

Credential type decides who owns the secret, and how it's injected. The resolved secret's value becomes the request's Authorization header, built per type: Raw ARN injects the value verbatim, no prefix; Bearer injects Bearer <value>; Basic injects Basic <base64(username:password)>.

Credential typeWhat you enterWho owns the secretAuthorization header
NoneNothingNo credential is sent
Raw ARNThe ARN of a Secrets Manager secret you already manageYou. The app only reads its value — create, rotate and delete it yourselfThe secret's value, verbatim
BasicUsername and passwordThe app. It stores {"username", "password"} as JSON in a secret it creates and manages for youBasic <base64(username:password)>
BearerA tokenThe app. It stores the token as a secret it creates and manages for youBearer <token>

For Basic and Bearer, the app names the secret deterministically (hyveon-{game}-healthcheck-auth, one per game) and creates or updates it itself when you save — you never see or enter an ARN for those two types.

The credential value never comes back. Every credential field (ARN, username, password, token) renders blank when you re-open the wizard or edit form. Whether one is already configured is signaled only by "a credential is already set" next to the Credential type selector — never the value, and independent of which type it was saved as.

Omitting vs. clearing on an edit works like every other secret field: leaving the fields for the current type blank keeps the existing credential unchanged; switching to None and saving clears it (deleting the app-owned secret first if the previous credential was Basic or Bearer). Switching from Basic/Bearer to Raw ARN and entering a new ARN also deletes the now-orphaned app-owned secret — but selecting Raw ARN and leaving that field blank counts as "unchanged," so the prior Basic/Bearer secret is left in place and still referenced. Removing the game entirely always deletes its app-owned secret, if any.

Step 4 — Storage

Two sections.

Volumes — "Every game server needs at least one EFS-backed volume for its save data." Each row has a Volume name (e.g. data) and a Container path (e.g. /data). The path must be absolute. The Remove button is disabled once only one row remains; you cannot go below one volume.

File seeds — "Optional — files written into a volume the first time the server starts." Each row has:

FieldPurpose
PathAbsolute path inside the container, e.g. /data/config.yml
ContentPlain-text file contents
Content (base64)Base64-encoded contents, for binary files
ModeUnix mode, e.g. 0644

File seeds can be removed down to zero. Blank fields are dropped entirely rather than written as empty strings.

Step 5 — Environment

Optional — environment variables injected into the container (e.g. EULA=TRUE).

Starts empty, with No environment variables configured. shown until you add a row. Add variable appends a row with a Variable name field and a Value field (both plain text). Like ports, there is no minimum — zero variables is valid, and every row's Remove button stays enabled all the way down to zero.

Two rules apply to the name field only — any string value is accepted for value:

  • A name must not be empty: environment[0].name must not be empty.
  • A name must not repeat another row's name in the same entry: environment[1].name "EULA" duplicates an earlier environment variable in the same entry.

There is no character-set or casing restriction on the name (unlike the game Name field in Step 1) — container images vary too much to assume a universal naming convention.

Step 6 — Review

A read-only summary in four cards — Identity, Resources, Networking, Storage — showing exactly what will be written. The Networking card's health-check sub-section (present only when one is enabled) shows the request shape and condition, and whether a credential is set — never the credential's value or ARN, the same convention the Storage card follows for file-seed contents. The Storage card lists file-seed paths only (contents are never displayed) and, underneath, an Environment variables list of name/value pairs — both sub-sections are omitted entirely when empty rather than shown blank.

The footer button reads Submit.

On success you get a minecraft created toast and are taken straight to the new game's detail page. The game appears in the table as Pending deploy until you apply.

How validation behaves

Errors appear as you type, not on blur or on submit. Next is disabled whenever the current step has any error; problems on other steps do not block you until you reach them.

On the Review step, Submit is disabled unless the whole draft is clean.

If the server rejects the submission, the dialog stays open with your draft intact and jumps to whichever step the first problem belongs to, with the message rendered against the offending field.

Draft autosave

Once you start typing, the wizard autosaves your in-progress draft — field values and which step you're on — about one second after you stop editing. Nothing is written for a wizard you open and immediately close without touching a field, and autosave pauses while a submission is in flight. If you close the dialog (Escape, overlay click, or the close control) less than a second after your last keystroke, the close itself flushes that pending save immediately rather than waiting for the debounce timer.

Autosave is best-effort, not guaranteed: it's written to local disk via the same store as the rest of the app's settings, and a save that fails (disk full, permissions, or any other write error) is logged and silently dropped rather than retried or surfaced to you — the wizard never interrupts you mid-edit over it. In the normal case, the debounce-plus- flush-on-close behavior above means you'd lose at most a few seconds of typing, but a persistently unwritable store means autosave doesn't help at all for that session.

Environment variable values and file-seed contents you type in are not included in what's autosaved or restored — see Draft resume and secrets below.

The draft is saved by the Electron main process, not kept in the page's own React state — so it survives more than just navigating away and back. Quitting and relaunching the whole app still leaves it in place. The next time you open the Games page, an orange banner appears above the table:

Unfinished draft: <name>Resume / Discard

Resume reopens the wizard pre-filled at the step you left off on. Discard deletes the saved draft and hides the banner without opening the wizard. The banner never opens the wizard on its own — resuming is always something you click.

There is only one draft slot: starting a second add-game draft (via Discard, then Add game again) overwrites whatever was saved before. Submitting successfully clears the saved draft along with the in-memory one; a failed submission (validation, conflict, or server error) leaves it in place so you can retry. If the saved entry is ever unreadable — for example after an app update changes its shape — it's treated as if no draft exists: the Games page loads normally and no banner appears.

This persistence is specific to the add-game wizard. The edit-game form does not autosave a draft, because it is always re-seeded from the live declared configuration when you open it — there is nothing to recover that reopening the form doesn't already give you.

Draft resume and secrets

Environment variable values (the Environment step), file-seed contents (the Storage step's file rows), and every health-check credential field — ARN, username, password, token (the Networking step's health-check block) — are never included in a resumed draft — only the variable/file names, the chosen credential type, and whether a credential is set, come back. If you'd typed a database password, a health-check credential ARN or basic/bearer credential, or other sensitive value before closing, you'll need to re-enter it after clicking Resume. This is deliberate: those fields are the most likely to hold something you'd consider a secret, so the app strips them out of what's read back to the wizard rather than round-tripping them through the Electron IPC layer on every resume.

The game detail screen

Clicking a name opens /games/:name.

A game detail page showing the In sync chip, Edit and Remove buttons, and cards for Container, Ports and Volumes

The header carries the game name, its status chip, and — for declared games — Edit and Remove game buttons. Below that:

CardContentsShown
ContainerImage, CPU, Memory, HTTPS (Enabled / Disabled)Always
PortsContainer port and protocol per rowAlways
VolumesName and container path per rowAlways
Environment variablesName/value pairsOnly if any are declared
File seedsA collapsed 3 files seeded at task start summary; expand for the paths (and modes). Contents are never shownOnly if any are declared
Connect messageThe raw message templateOnly if set

The ghost-entry variant

If you open the detail page for an Undeclared game, none of those cards appear. Instead:

This game is deployed but has no entry in the configuration object — there is no declared configuration to show.

Edit and Remove game are not rendered at all, because there is nothing declared left to edit or remove. The way to clean it up is to run a plan and apply from the Infrastructure page, which destroys the orphaned AWS resources.

If you navigate to a name that exists in neither place you get No game named "foo" was found.

Editing a game

Edit replaces the detail cards with a flat form containing the same five sections as the wizard — Identity, Resources, Networking, Storage, Environment — pre-filled from the current declaration. All the same validation applies, but it applies to the whole form at once rather than step by step.

The Name field is disabled. Renaming a game is a delete-and-recreate, not an update: the name is the task-definition family, the EFS access-point key, the log-group name and the DNS label. Remove the old game and add a new one if you need a different name.

https has the same toggle here as in the wizard's Networking step, and the same health-check block sits below it — enable/disable, request/condition fields, and the credential-type selector with its per-type fields, identical to Step 3 of the wizard (including "a credential is already set" next to the selector, and every credential field rendering blank rather than echoing back a previous value). environment is directly editable here too, in its own Environment card — the same row editor (Variable name / Value, no minimum row count) as the wizard's Step 5.

Above the save button:

Saving only updates deployment-config.json — visit Infrastructure to apply this change to the live server.

Save changes writes the versioned JSON configuration object (deployment-config.json, in your S3 configuration bucket) and returns you to the read-only view with the new values — it does not touch AWS by itself. A separate plan/apply run from the Infrastructure page is still required to make the change live, the same as adding a game. Ports declared in this form are checked against every other game, so a game never collides with itself.

Removing a game

Remove game opens a confirmation dialog:

Remove minecraft?

This deletes the minecraft entry from deployment-config.json. The deployed AWS resources stay live until an operator applies the change from the Infrastructure page.

Below the text is a single input whose placeholder is the game's own name. You must type the game name exactly — the match is case-sensitive and untrimmed — before the Remove game button becomes clickable.

On success you get minecraft removed from deployment-config.json and are returned to the games list, where the game now shows as Undeclared until you run a plan and apply.

Concurrent-edit conflicts

The app stores your deployment configuration as a versioned object in your S3 configuration bucket (there is no local-file mode), and can guard writes with an optimistic-concurrency check: the write carries the object version the app last read, and S3 rejects it if someone else has written since. That surfaces as:

Optimistic lock failed: expected etag "abc123" but remote is now "def456".

There is no automatic retry. Reload the games list to pick up the newer state and redo your change.

One honest caveat about this mechanism as it stands:

  • The app's own screens do not currently opt in. The wizard, the edit form and the remove dialog all issue unconditional writes, so in practice you will not see this error from the UI today. The plumbing exists end to end and is exercised by the test suite; the UI simply does not send the expected version yet. If two people are editing the same configuration concurrently, treat it as last-write-wins and coordinate out of band.