Releases: haydenbleasel/ghost
Release list
v2.0.0
Major Changes
-
#76
e133e1fThanks @haydenbleasel! - Rework Ghost from a hosted multi-tenant platform into a self-hosted, single-tenant deployment you run on your own Vercel account.Breaking: this release requires a fresh deployment. The database schema is incompatible with 1.x (migrations were squashed to a new baseline) and the auth/credential model changed entirely.
- Deploy your own — the README now leads with a Vercel Deploy Button that clones the repo, provisions Neon Postgres, Upstash Redis, and Vercel Blob, and prompts for three secrets:
HETZNER_API_TOKEN,AUTH_PASSWORD, andGHOST_SECRET. - Env-credential auth — Better Auth is removed (along with sign-up, passkeys, profiles, and avatars). The owner signs in with
AUTH_EMAIL/AUTH_PASSWORDfrom the environment; sessions are a jose-signed JWT cookie verified fully in the edge middleware. - Hetzner token from the environment —
HETZNER_API_TOKENreplaces the per-user encrypted token that previously lived in Postgres, and the token-management UI is gone. - Single-tenant schema — the
User,Session,Account,Verification, andPasskeymodels are dropped;userIdscoping is removed from servers and snapshot builds; golden images are keyed per deployment environment. - One signing secret —
GHOST_SECRETreplacesBETTER_AUTH_SECRET,SESSION_SECRET, andBOOTSTRAP_JWT_SECRET; each token type keeps its own issuer/audience pair. - App at root — the dashboard now lives at
/(with/new,/account,/[id]); the marketing site is removed and its content (features, supported games, how-it-works, animated logo) moved into the README. - Leaner by default — Sentry, PostHog, and Google Analytics are removed, along with Dependabot; React Compiler is enabled; all dependencies bumped (and zod unpinned now that
@workflow/worldno longer requires an exact version).
Reconnecting servers created by a 1.x instance. Your VMs live in your own Hetzner account, so retiring the old control plane doesn't stop the games — players stay connected and nothing is deleted. But 2.0 starts with a fresh database, so those servers won't appear in your new dashboard. To move a world across:
- Copy the game data off the old VM. Open the VM's console in the Hetzner Cloud dashboard (or SSH if you added a key) and archive the game's data directory:
tar -czf /root/world.tar.gz -C /var/lib/ghost/game/data .— then get the archive somewhere fetchable (e.g.scpit off, or upload it to any file host). - Create a replacement server for the same game on your new instance and let it provision.
- Restore the data. Stop the new server from the dashboard, open the new VM's Hetzner console, and extract the archive over the data directory:
curl -L <archive-url> | tar -xz -C /var/lib/ghost/game/data— then start the server again. (For a handful of individual files, the dashboard's Files tab can upload them directly instead.) - Delete the old VM from the Hetzner console once you've confirmed the world came across, so it stops billing. Old VMs are harmless in the meantime — their agents just long-poll the retired control-plane URL and get nothing.
If you'd rather start fresh, step 4 is the only one you need.
- Deploy your own — the README now leads with a Vercel Deploy Button that clones the repo, provisions Neon Postgres, Upstash Redis, and Vercel Blob, and prompts for three secrets:
Patch Changes
-
#55
9f5d0feThanks @tomnieuwland! - Add Factorio support, using the factoriotools/factorio:stable image. Space Age DLC is enabled by default and toggleable. Supports Factorio Username/Password for server discovery. -
#56
274f368Thanks @tomnieuwland! - Adds a script for generating docker compose files from defined games for local testing -
cf52b97Thanks @haydenbleasel! - Report agent command results accurately when the ack POST fails transiently — a succeeded command was re-acked as failed and the remaining commands in the envelope were abandoned; acks are now best-effort and isolated from command execution -
bcf6715Thanks @haydenbleasel! - Make agent enrollment survive a lost response — one dropped enroll reply used to brick the VM forever (each retry generated a fresh keypair and hit "Token already used" in a fatal loop, forcing server deletion); the agent now persists its keypair before the enroll POST and the server replays the success idempotently when a burned token arrives with the already-enrolled public key -
cf52b97Thanks @haydenbleasel! - Fix lost agent events: a failed event flush never scheduled a retry (terminal events like 'Game stopped' could be dropped forever on a quiet agent), and concurrent flushes silently no-oped, letting shutdown discard buffered events mid-flight; flushes are now serialized and rescheduled on failure -
cf52b97Thanks @haydenbleasel! - Fix every agent file command (list, delete, install-from-URL) crashing with a ReferenceError — agent/src/files.ts used node:path helpers it never imported; the agent package is now included in the repo typecheck so this class of bug can't ship again -
cf52b97Thanks @haydenbleasel! - Clean up the temporary .part download when installing a file from URL fails at the final rename step, instead of leaking up to 500MB per failed attempt into the game data directory -
cf52b97Thanks @haydenbleasel! - Stop replaying the last 200 historical log lines on every start/restart — the log tail now follows from the triggering command's start time, so old lines no longer reappear in the console with fresh sequence numbers -
bcf6715Thanks @haydenbleasel! - Re-attach the console log tail when the agent boots with the game already running — after an agent crash or VM reboot the tail only started on the next user-issued start/restart, so the dashboard console silently went dead; the agent now probes the container on boot and tails from boot time (no historical replay) -
cf52b97Thanks @haydenbleasel! - Prevent concurrent agent state saves from racing on a shared temp file, which could crash a just-executed command or corrupt state.json and brick the agent on next boot; saves are now serialized and use unique temp files -
cf52b97Thanks @haydenbleasel! - Return 401 JSON from authenticated API routes when the session has expired instead of redirecting to the sign-in page — fetch/EventSource consumers were receiving the sign-in HTML with a 200, causing endless JSON parse errors and a dashboard that never noticed it was signed out; the server page poll now redirects to sign-in on 401 -
bcf6715Thanks @haydenbleasel! - Recover snapshot builds whose builder-VM create response was lost — the retry's unique-name conflict was turned straight into a fatal build failure, stranding a billedghost-builder-*VM that nothing tracked or deleted; the step now adopts the existing VM by its deterministic name, and transient 429 rate limits retry instead of failing the build -
cf52b97Thanks @haydenbleasel! - Redeliver commands whose delivery or ack was lost in transit — commands were marked delivered before the agent received them with no redelivery path, so a dropped long-poll response left them stuck forever; the agent now dedupes redeliveries against its recent command history -
cf52b97Thanks @haydenbleasel! - Escape dollar signs in generated docker compose files — Docker Compose interpolates$VAR/$ {VAR} in every string, so a server name like 'Ca$h Grab' was silently mangled and an unclosed '${' made compose fail to start entirely; applies to all games including Factorio's literal config blocks -
bcf6715Thanks @haydenbleasel! - Apply the observed-state transition when a retried agent event dedupes — if the first attempt crashed between inserting the event and updating the server row, the retry's duplicate hit returned early and the server was stuck showing "running" after the game had stopped (also breaking...
v1.2.0
Minor Changes
- #53
2363908Thanks @haydenbleasel! - Add Left 4 Dead 2 as a supported game. Uses theleft4devops/l4d2image with a docker-compose inlineserver.cfgfor cvars that need to survive map loads (z_difficulty,sv_allow_lobby_connect_only). Join passwords are disabled — L4D2's coop lobby flow drops clients aftersv_passwordverification on direct connect, so servers stay private via unlisted IP/port instead.
Patch Changes
-
#67
f1e0609Thanks @haydenbleasel! - Return 400 instead of an unhandled 500 when the heartbeat, events, or command-ack agent routes receive a signed request with a malformed JSON body.JSON.parsesat outside the routes' error handling, unlike the other agent endpoints. -
#67
f1e0609Thanks @haydenbleasel! - Block symlink path traversal in agent file operations.resolveInRootonly normalized the path string, so a symlink planted inside the bind-mounted data root by a compromised game container could redirectFILES_INSTALL_FROM_URL/FILES_DELETEwrites outside the root (the agent runs as root on the host). Paths are now resolved throughrealpathon their deepest existing ancestor and re-verified against the real data root before any read, write, or delete. -
#67
f1e0609Thanks @haydenbleasel! - Always validate game settings when creating a server. A request that omitted thesettingsfield skipped validation entirely, bypassing required fields like Don't Starve Together's cluster token and provisioning a paid VM that could never start. -
#67
f1e0609Thanks @haydenbleasel! - Prevent double-delivery of agent commands.claimPendingCommandsdid a find-then-update, so two overlapping long-polls (e.g. an agent retry racing a still-running handler) could both read the same pending rows and execute a command — including destructive ones likeFILES_DELETE— twice. Each row is now claimed with a conditional update onstatus: "pending", so exactly one caller wins it. -
#67
f1e0609Thanks @haydenbleasel! - Write the agent's state file atomically.saveStatewrote a temp file but then copied it into place with a truncate-and-write, so a crash mid-save could leave a corruptstate.jsonthat permanently bricked the agent on next boot. The temp file is now swapped in withrename. -
#67
f1e0609Thanks @haydenbleasel! - Fetch all pages when listing a server's backups and snapshots.listImagesForServerrequested a single page of 50 images for the whole account and filtered client-side, so servers on accounts with more than 50 images silently lost backups from the UI. Backups are now filtered server-side via Hetzner'sbound_toparameter and both listings followmeta.pagination.next_pageto exhaustion. -
#67
f1e0609Thanks @haydenbleasel! - Make snapshot builder VM creation idempotent.stepCreateBuilderVmnow returns the already-persistedproviderBuilderIdon retry instead of creating another VM, and the builder name is derived deterministically from the build id so a retried create whose first response was lost is rejected by the provider's unique-name constraint rather than silently billing a second orphaned VM. -
#67
f1e0609Thanks @haydenbleasel! - Harden docker-compose value escaping.escapeComposeValueonly escaped double quotes, but values are emitted inside YAML double-quoted scalars where\is the escape character — a backslash in a server name or game setting (e.g. world name, MOTD) produced invalid YAML or terminated the scalar early, and embedded newlines could inject additional YAML lines into the root-run compose file. Backslashes are now escaped before quotes and control characters are stripped. -
#67
f1e0609Thanks @haydenbleasel! - Stopdocker composeinvocations from hanging the agent. The subprocess was spawned with piped stdout/stderr that were never read, so any compose command producing more than the OS pipe buffer (~64KB, easily hit by image-pull progress) blocked forever with no timeout. Both pipes are now drained while awaiting exit, and compose failures include the last stderr line for easier debugging. -
#67
f1e0609Thanks @haydenbleasel! - Make agent enrollment atomic. The bootstrap token was burned in a transaction but the agent row was created after it committed, so a transient create failure left the token burned with no agent — the VM's retries then hit "Token already used" forever and the server had to be re-provisioned. The burn is now a conditional update inside the same transaction as the create, which also closes the race where two concurrent requests could both pass the burned-token check. -
#67
f1e0609Thanks @haydenbleasel! - Deduplicate retried agent event batches. When the agent re-sent a batch after a lost response, the server assigned fresh ids and seqs, duplicating activity timeline entries and log lines. Agent-sourced rows now use deterministic ids derived from the protocol'sclientEventId(activity) andagentSeq(logs), scoped by agent id, so replayed inserts are dropped. -
#53
2363908Thanks @haydenbleasel! - Regenerate the Hetzner OpenAPI types. Picks up@deprecatedmarkers on the singularget_<resource>_actionendpoints (per the 2026-04-30 changelog), the Load Balancer service/target type split, the primary/secondary Zone schema split, and per-location pricing breakdowns. None of the endpoints we call were affected. -
#67
f1e0609Thanks @haydenbleasel! - Close the console log stream on unmount.LogsStream's effect cleanup only set a flag and never closed the liveEventSourceor cleared the pending reconnect timer, so navigating away from the Console tab left the SSE connection (and the server's 500ms log poll loop) running for up to the route's 300smaxDuration, with stale listeners still updating state. -
#67
f1e0609Thanks @haydenbleasel! - Size the Minecraft JVM heap from the actual machine instead of a hardcoded6G. Rescaling a Minecraft server previously had no effect on the game (and on machines smaller than the default the fixed heap could exceed total RAM). The compose builder now receives the provider-reported machine memory and gives the JVM ~75% of it; the heap is applied whenever a new compose is pushed (install and settings updates). -
#67
f1e0609Thanks @haydenbleasel! - Stop reporting transient provisioning errors as "Owner has not configured provider credentials". The catch instepCreateProviderServerswallowed every error from the provider lookup (including DB blips) into a permanentFatalError; it now only convertsMissingProviderCredentialsErrorand lets everything else retry. -
5c50664Thanks @haydenbleasel! - Refactor the Hetzner integration behind a provider-agnosticProviderinterface so a future AWS or GCP backend can be added by writing a new implementation rather than rewriting call sites.lib/hetzner/is replaced bylib/providers/(interface + types + per-user factory) withlib/providers/hetzner/as the only implement...
v1.1.1
Patch Changes
-
#48
e12b9e0Thanks @haydenbleasel! - FixServer.observedStatenot transitioning when the agent runs aSTOPorSTART. Previously the agent emitted aphase: "stopped"activity event afterdocker compose stop, but nothing reconciledServer.observedStatefrom the activity stream — so the badge stayed on"running"and users saw "the server does not stop" even though it had.emitActivitynow updatesServer.observedStatefor the agent-driven steady-state phases (stopped→"stopped",healthy→"running"); the provisioning workflow continues to own its own transitions. TheSTARThandler now also enqueues ahealthyevent aftercomposeUp()so the badge flips back to"running", the agent'sSTOPhandler kills the log tail aftercomposeStop()so container shutdown messages reach the Console tab, andexecuteCommandlogs start/success/failure so the agent's process console is no longer silent. Also disables the Delete menu item oncedesiredStateis"deleted"so users can't re-trigger teardown on a server that's already being deleted. -
#48
e12b9e0Thanks @haydenbleasel! - Refresh marketing branding: new logo, Geist Pixel display font, redesigned homepage, and updated favicon and Open Graph image. -
#48
e12b9e0Thanks @haydenbleasel! - Scope the Hetzner builder snapshot'sdescriptionfield bySNAPSHOT_ENVIRONMENT(ghost-gold-production,ghost-gold-preview-<branch>,ghost-gold-development) instead of a sharedghost-goldliteral. The labels already scoped which snapshot a deployment uses for provisioning; this aligns the human-readable description so production / preview / local images are easy to tell apart in the Hetzner dashboard and won't be confused for one another during cleanup. -
#48
e12b9e0Thanks @haydenbleasel! - ReplacePromise.race(hook, sleep, ...)blocks inprovisionServerwith polling loops that read state from the database. Eliminates theWorkflow run completed with N uncommitted operation(s): sleepwarnings emitted when hooks won the race and left orphan timers ticking on the backend. Cancellation now propagates throughdesiredStatepolling (worst-case latency: one poll interval — 6s during boot/enroll, 10s during install) instead of a hook, and the install wait reads the latest agent-reported phase fromactivity_eventsinstead of subscribing to the phase hook.
v1.1.0
Minor Changes
-
#47
6080d27Thanks @haydenbleasel! - Add support for Counter-Strike 2 dedicated servers. Configurable game mode, starting map, max players, bot quota and difficulty, LAN mode, and Steam GSLT for public servers. -
#47
6080d27Thanks @haydenbleasel! - Add support for Don't Starve Together dedicated servers. Configurable game mode, server intention, max players, PvP, and Klei cluster token. -
#47
6080d27Thanks @haydenbleasel! - Add support for Satisfactory dedicated servers. -
#47
6080d27Thanks @haydenbleasel! - Add support for V Rising dedicated servers.
Patch Changes
743f3c0Thanks @haydenbleasel! - Pinzodto4.3.6to match the version expected by@workflow/worldand@workflow/world-vercel. Zod 4.4 changed how discriminated unions handle fields overridden withz.undefined()via.extend(), causing the workflow runtime to throwSchema validation failed for POST /v3/runs/.../eventson every callback and leaving runs wedged in thependingstate.
v1.0.0
Major Changes
fe9c356Thanks @haydenbleasel! - Initial release