Self-hosted version of the game: Discord/GitHub OAuth login, SQLite for persistence, and server-authoritative offline progress (hard-capped at 72 hours regardless of upgrades).
server/- Express API. Passport handles the Discord/GitHub OAuth handshake; on success we issue our own JWT in an httpOnly cookie (no server-side session store needed). SQLite (better-sqlite3) holds two tables:usersandsaves(one JSON blob per user).server/gameLogic.js- a server-side mirror of the client's production math, used only to compute how much a player produced while away. If you change tier costs, production rates, or upgrade effects inclient/src/RackStack.jsx, mirror the change here too, or offline and online production will drift apart.client/- the game itself (Vite + React + Tailwind + lucide-react), talking to the API instead of browser storage.
Offline progress works like this: the client ticks production locally in
real time while a tab is open and POSTs its state to /api/save every 5s.
On next load (GET /api/save), the server computes elapsed time since the
last save, capped at 72h, applies the same production formulas, persists the
caught-up result, and returns it - the client just displays what it's given.
There's no always-on background worker simulating idle games 24/7; that's
unnecessary for an idle game and would just waste resources.
Discord: https://discord.com/developers/applications -> New Application
-> OAuth2 -> add a redirect URI: https://<your-domain>/auth/discord/callback
-> copy the Client ID and Client Secret.
GitHub: https://github.com/settings/developers -> New OAuth App
-> Authorization callback URL: https://<your-domain>/auth/github/callback
-> copy the Client ID and Client Secret.
You only need to configure the provider(s) you actually want to use - leave
the other's ID/SECRET blank in .env and its login button will just fail if
clicked (harmless, but you may want to hide it later).
cp .env.example .env
openssl rand -hex 32 # paste the output in as JWT_SECRET
# fill in DISCORD_/GITHUB_ client id, secret, and callback URLsThe callback URLs must exactly match what you registered with Discord/GitHub,
including the scheme (https://) - if you're putting this behind the
Cloudflare tunnel you already use for other services, point a subdomain at
this container and use that in both places.
docker compose up -d --buildThis builds the client, starts the server on port 3000 (mapped in
docker-compose.yml - change the host side if you want a different port),
and persists the SQLite file to ./data/rackstack.db via a bind-mounted
volume, so it survives container rebuilds.
Point your reverse proxy / Cloudflare tunnel at http://<host>:3000.
Two processes:
# terminal 1 - API server
cp .env.example .env # fill in values, DISCORD/GITHUB callback URLs can be http://localhost:3000/auth/.../callback for local testing
npm install
npm run dev
# terminal 2 - client with hot reload, proxies /api and /auth to :3000
cd client
npm install
npm run devVisit the client dev server's printed URL (usually http://localhost:5173).
- SQLite, not Postgres: chosen for zero-config, single-file persistence
that's trivial to back up (
cp data/rackstack.db data/rackstack.db.bak). Fine for a personal or small-group deployment. If you outgrow it (many concurrent users, wanting replication, etc.), theserver/db.jsmodule is the only place that touches the database - swapping it for apgversion behind the same function signatures (upsertUser,getSave,putSave,deleteSave) wouldn't require touching routes or game logic at all. - JWT cookie, not sessions: avoids needing a session store. The cookie
is httpOnly and
securein production, valid for 90 days. - Multi-user by default: every Discord/GitHub login gets its own
save, keyed by
provider:providerId. If you want this to be just-you, nothing extra to do - your account is simply the only one with data. - 72h offline cap is a hard ceiling in
gameLogic.js(OFFLINE_CAP_HOURS), applied on top of whatever the Extended Uptime upgrade computes, so no upgrade or future change can push past it.