A full implementation of SHIFT (the "steal and guess" multiplayer game) per
SHIFT_Website_Requirements_Document.md and SHIFT_UI_Design_Requirements.md,
built in Go (stdlib-heavy backend) with a server-rendered + vanilla-JS frontend
implementing the "Vault Card" design system.
Start here: progress.md — full checklist of what's built, what's stubbed, and
why (this sandbox has no live Postgres and no Go-module-proxy access, so a few
spec'd third-party pieces were reimplemented on the stdlib — all documented there
with exact swap-back instructions).
- Go 1.22+ (no external modules needed — the whole backend is stdlib-only)
cd shift-website
go run ./cmd/serverThen open http://localhost:8080/register in two different browser profiles (or one normal + one incognito window) to register two accounts and play a real match against yourself, or open one and use "Play vs Computer" from the home page.
Data persists to shift_data.json in the working directory (see progress.md —
this stands in for PostgreSQL in this environment). Delete that file to reset all
accounts/matches; the object catalog reseeds itself automatically on next boot.
cmd/server/main.go — entrypoint, HTTP route wiring (net/http ServeMux)
cmd/smoketest/main.go — scripted two-client WS test (see below)
internal/store/ — Store interface + JSON file-backed implementation
internal/auth/ — registration, recovery, sessions, hashing, rate limiting
internal/game/ — rank/level/difficulty config, object catalog, room actor
internal/lobby/ — challenge manager (create/respond/expire)
internal/ws/ — hand-rolled WebSocket server + connection hub
internal/api/ — leaderboard/profile/settings/catalog HTTP handlers
internal/webui/ — HTML template rendering
web/templates/ — server-rendered pages (register/home/lobby/room/…)
web/static/css/main.css — full design-token system + Vault Card component
web/static/js/ — common.js, wsclient.js, ai.js (vs-Computer engine)
migrations/ — real PostgreSQL DDL + seed data (see progress.md)
cmd/smoketest drives two real WebSocket connections through registration →
lobby → challenge → accept → ready-up → a full round of steal/guess, asserting
the server responds correctly at each step (no mocking — it's the same protocol
a browser tab uses). Run it against a live server:
go run ./cmd/server & # start the server first
go run ./cmd/smoketest # then run the scripted clientA full 10-round match takes a couple of minutes to play out automatically at
easy difficulty timings; the smoke test's own internal deadline is generous
enough to let a full match finish if you want to watch match_complete fire.
The app is a single dependency-free Go binary (see "no external modules" below), so the Docker build is fast and needs no network access to a Go module proxy.
Steps:
- Push this project to a GitHub repo (or use
railway upfrom this directory with the Railway CLI). - In Railway, create a new project → Deploy from GitHub repo (or run
railway initthenrailway upfrom this folder). Railway will detect theDockerfileandrailway.jsonautomatically and build with Docker — no other config needed. - Railway sets
$PORTautomatically; the app already reads it (cmd/server/main.go), so no env var setup is required to get it running. - Once deployed, open the generated
*.up.railway.appURL —/registeris the entry point.
About data persistence (important): this build uses internal/store.FileStore
(a JSON file) in place of PostgreSQL — see progress.md Section 0 for why. Railway's
container filesystem is ephemeral: every redeploy wipes /app/data, so
accounts/matches will reset each time you push a new version. Two ways to handle
this while testing:
- Fine for quick functional testing — just re-register test accounts after each deploy, nothing else to configure.
- Want persistence across deploys? Attach a Railway Volume
mounted at
/app/datain the service settings. The Dockerfile already setsSHIFT_DATA_PATH=/app/data/shift_data.jsonand creates that directory, so once a volume is mounted there, data survives redeploys automatically — no code changes needed. - Want the real thing? Add a Railway PostgreSQL plugin and implement
store.Storeagainst it using/migrations(see "Moving to production" below) — this is the actual long-term path, the file store is a sandbox stand-in.
Session cookies automatically get the Secure flag when RAILWAY_ENVIRONMENT
is set (Railway sets this for you), since Railway terminates TLS at the edge.
WebSockets work out of the box on Railway — no special proxy config needed.
go.mod has zero require lines — the entire backend is Go standard library
only (see progress.md Section 0 for what was hand-rolled and why: a WebSocket
server, PBKDF2 password hashing, UUID generation). This means go build never
touches the network, which is convenient for Docker builds and CI alike.
- Database: stand up PostgreSQL, run
migrations/0001_init.sqlthenmigrations/0002_seed_object_catalog.sql, implementstore.Storeagainst it (e.g. withpgx), and swap the one line incmd/server/main.gothat constructsstore.NewFileStore(...). - WebSocket library: swap
internal/ws/websocket.go's hand-rolled framing forgorilla/websocketif desired — same external behavior, just less code to maintain long-term. Not required; the current implementation is a full RFC 6455 server (text frames, fragmentation, ping/pong, masking). - Password hashing: swap
internal/auth/hash.go's PBKDF2 implementation forgolang.org/x/crypto/bcryptif you'd like to match the spec's exact library choice — functionally equivalent either way (see progress.md). - TLS / reverse proxy: put this behind Caddy or nginx for TLS termination in
production; the app itself serves plain HTTP on
:8080.
See progress.md for the complete, itemized checklist against both source spec
documents.