# Stack Research

**Domain:** Real-time multiplayer 2D web game (server-authoritative, <50 CCU, single Fly.io machine, browser client)
**Researched:** 2026-05-01
**Confidence:** HIGH (versions verified via `npm view` 2026-05-01; ecosystem positioning verified across multiple 2026 sources)

---

## Executive Recommendation

For REBNO's MVP gate (two players join a room, move, chat) and full parity through <50 CCU:

- **Server:** Node 22 LTS + TypeScript, **Colyseus 0.17.10** on top of `ws` 8.20.0 (Colyseus default). Skip raw `ws` and skip `uWebSockets.js` for v1 — Colyseus subsumes the room/state-sync/matchmaking layer that you'd otherwise have to write by hand mirroring 39dll, and `ws` is more than fast enough for 50 CCU.
- **Client engine:** **Phaser 3.90.0 "Tsugumi"** (NOT Phaser 4 yet — see rationale). PixiJS only if Phaser's tilemap/scene model proves wrong for the GM5 paradigm during Stage 2.
- **Wire serialization:** **Colyseus's built-in `@colyseus/schema` 4.0.21** for room state delta sync; **`msgpackr` 1.11.10** for ad-hoc client→server intent messages outside the schema (chat, RPC).
- **Persistence:** **`better-sqlite3` 12.9.0 + Litestream** to a Tigris bucket. Postgres is wrong for this scale; the server is single-machine and stateful by design.
- **Auth:** **Better-Auth 1.6.9** with `argon2` 0.44.0 hashing. (Lucia is officially deprecated as of March 2025.)
- **Build:** **Vite 8.0.10** for the client; `tsx` for server dev, `tsc` for prod build.
- **Monorepo:** **pnpm workspaces** alone — Turborepo's caching is overkill for a 3-package repo and adds config burden you don't need.
- **Hosting:** Fly.io single machine, region `iad` or owner-local, Fly Volume for SQLite, Tigris bucket for both Litestream replicas and (later) static asset CDN.

---

## Recommended Stack

### Core Technologies

| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| Node.js | 22 LTS | Server runtime | Active LTS through 2027-04. Native `fetch`, `WebSocket` client, stable test runner. Colyseus, `better-sqlite3`, and `argon2` all build clean against N-API on Node 22. |
| TypeScript | 5.6+ | Source language both halves | Shared packet/schema types between client and server is the single biggest leverage point of this stack vs the original 39dll byte-stream chaos. |
| Colyseus | 0.17.10 | Authoritative game server framework | Solves rooms, matchmaking, state delta sync, reconnection, presence — the exact layer the original GMS server hand-rolled with 39dll. Saves ~6–10 weeks of plumbing. Uses `ws` under the hood; you can drop down to raw frames when needed. |
| @colyseus/schema | 4.0.21 | Binary state sync with delta encoding | Decorator-based schema → automatic binary delta packets. Replaces the entire "what bytes did I `writebyte`/`writedouble` in what order" problem from the 39dll era with a typed schema you author once. |
| Phaser | 3.90.0 | 2D game engine (client) | Closest paradigm match to GameMaker: scenes ≈ rooms, sprites/tweens/animations/cameras/tilemaps/input/audio all included and integrated. Single-package install, no glue libraries to pick. **See Phaser 4 caveat below.** |
| Vite | 8.0.10 | Client dev server + prod bundler | Official Phaser template uses it. Native TS, fast HMR, zero-config asset handling for sprites/audio. `vite build` ships an optimized static bundle Fly can serve directly or you push to Tigris. |
| pnpm | 10.x | Package manager + workspace | Strict isolation prevents accidentally importing server-only deps into the client bundle. `workspace:*` protocol makes the shared package edge explicit. Disk-efficient, fast CI. |
| Better-Auth | 1.6.9 | Auth framework | Lucia is deprecated (March 2025). Better-Auth is the de facto 2026 replacement: session management, password + OAuth + passkeys, framework-agnostic, drizzle/sqlite adapter native. Handles cookie session middleware so you don't roll it yourself. |
| argon2 | 0.44.0 | Password hashing | OWASP 2026 #1 recommendation (Argon2id). Native binding, fast on server CPU. Used by Better-Auth's `password` provider. |
| better-sqlite3 | 12.9.0 | DB driver (synchronous, in-process) | Synchronous API maps perfectly to a single-tick game server loop — no await spaghetti for player save/load. ~10× faster than `node-sqlite` for the small-row, high-frequency reads a game server does. |
| Drizzle ORM | 0.45.2 | TypeScript SQL builder | Code-first schemas in TS (no separate `.prisma` file), no codegen step, instant type updates. Native `better-sqlite3` driver. Bundle size ~7 KB vs Prisma's 1.6 MB engine. |
| Litestream | 0.3.x | Continuous SQLite replication to S3/Tigris | Streams every WAL frame to object storage. Disaster-recovery without standing up a separate DB tier. Designed by the Fly team explicitly for the Fly.io + persistent-volume + SQLite pattern. |
| Fly.io | — | Host | Single VM with persistent volume = ideal for stateful WS server. Cheap at <50 CCU (free tier or ~$5/mo). Tigris S3-compatible bucket included for Litestream and static assets. |

### Supporting Libraries

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| msgpackr | 1.11.10 | Schemaless binary serialization | Chat messages, debug RPC, anything outside the Colyseus state schema. ~3× faster than `@msgpack/msgpack` per 2026 benchmarks; drop-in replacement. |
| ws | 8.20.0 | Raw WebSocket lib | Already a transitive dep via Colyseus. Don't import directly unless you need a non-Colyseus endpoint (e.g., a `/health` or admin channel). |
| zod | 3.x | Runtime validation for client→server intents | Validate untrusted client payloads before they hit the simulation. Pair with Colyseus `onMessage` handlers. |
| pino | 9.x | Structured JSON logging | Fly.io ingests JSON logs natively; `pino-pretty` for local dev. |
| @fly-apps/litefs (optional) | — | If you ever need read-replicas | NOT for v1. Mentioned for completeness; <50 CCU on a single machine never needs it. |
| vitest | 3.x | Test runner | Same engine as Vite, runs both client and server suites with shared config. |
| tsx | 4.x | Dev-time TS runner for the server | Hot-reload server on file changes during development. |

### Development Tools

| Tool | Purpose | Notes |
|------|---------|-------|
| pnpm | Workspace + install | `pnpm install` from repo root; `pnpm --filter server dev`, `pnpm --filter client dev`. |
| Vite | Client dev server | Run `pnpm --filter client dev`; HMR connects to local server over WS. |
| tsx watch | Server dev runner | `tsx watch src/index.ts` — restarts on file change. |
| flyctl | Fly.io deploy CLI | `fly deploy` from `apps/server`. Volume + Tigris bucket via `fly storage create`. |
| Phaser Editor v5 | Optional visual scene editor | Has Phaser 4 support; for v3 use the v4-1 release which still ships v3 templates. Optional — Phaser scenes are just TS code. |
| Tiled Map Editor | Tilemap authoring | Phaser parses Tiled JSON natively. Replaces hand-laid GM5 room editor for new content; original room data should be extracted from `.gmd` and round-tripped to Tiled JSON. |

---

## Installation

```bash
# Repo root
pnpm init
# pnpm-workspace.yaml: packages: ['apps/*', 'packages/*']

# Shared types package (packages/shared)
pnpm --filter shared add -D typescript

# Server (apps/server)
pnpm --filter server add colyseus @colyseus/schema @colyseus/monitor
pnpm --filter server add better-sqlite3 drizzle-orm
pnpm --filter server add better-auth argon2
pnpm --filter server add msgpackr zod pino
pnpm --filter server add -D typescript tsx vitest @types/node @types/better-sqlite3 drizzle-kit

# Client (apps/client)
pnpm --filter client add phaser colyseus.js msgpackr
pnpm --filter client add -D typescript vite vitest

# Cross-package wiring (in apps/server/package.json and apps/client/package.json)
# "@rebno/shared": "workspace:*"
```

---

## Alternatives Considered

| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| Colyseus | Geckos.io | Only if you measure tangible jitter problems with TCP/WS at your target latencies. Geckos uses WebRTC DataChannels for unreliable UDP — the right answer for fast-twitch shooters, not for a top-down RPG-style world with chat. Adds STUN/TURN ops complexity. |
| Colyseus | Nakama | Larger team, multiple titles, want a unified backend across games. Heavier to operate (Go binary + DB), overkill for one Chrome-only title <50 CCU. |
| Colyseus | Raw `ws` + custom protocol | Educational value only. You'd reinvent Colyseus's room manager, state delta encoder, matchmaker, and reconnect handler — the exact wheel that bit the original 39dll codebase. Skip it. |
| Phaser 3 | Phaser 4 (4.1.0, April 2026) | If starting after ~Q3 2026 once the v4 ecosystem (plugins, tutorials, AI scaffolding) catches up. v4 is faster and architecturally cleaner but still settling. For a "ship MVP fast" milestone today, v3.90 is the safe choice. **Decision gate: re-evaluate at Stage 6 start.** |
| Phaser 3 | PixiJS 8.18.1 | If Stage 2 catalogue reveals heavy custom-shader / particle-VFX / non-standard-rendering needs that fight Phaser's scene model. PixiJS is a renderer, not a framework — you'd add `@pixi/tilemap`, audio lib, input lib, scene lib separately. Worth it only if Phaser is the wrong shape. |
| `@colyseus/schema` | protobufjs / @bufbuild/protobuf | If you want a wire format readable by non-JS clients later (mobile/desktop port). For a Chrome-only client sharing types with a TS server, schema is strictly better — zero codegen step, integrated delta encoding, designed for game state. |
| `@colyseus/schema` | flatbuffers | Zero-copy access matters for huge state blobs (megabytes per tick). At <50 players and a small world, the throughput and DX of `@colyseus/schema` win. Reconsider only if profiling shows deserialization hot. |
| `msgpackr` | `@msgpack/msgpack` | `msgpackr` is faster (~3×) and API-compatible. Use the slower one only if you hit a bug; you won't. |
| Better-Auth | Lucia | **Don't.** Lucia v3 was deprecated March 2025 — the maintainer explicitly recommends Better-Auth as the path forward. |
| Better-Auth | Hand-rolled bcrypt + cookie sessions | Possible but you'll re-implement CSRF, session rotation, password reset flow, OAuth (if ever). Better-Auth gives you all of it with one config object. |
| better-sqlite3 + Litestream | Fly Postgres + Prisma | Postgres makes sense at >5K CCU, multi-region writes, or schema-heavy relational workloads. Your data model (account, inventory, message-board log) is small, single-writer, and read-mostly — exactly SQLite's sweet spot. Litestream gives you S3 backups without the Postgres ops surface. |
| better-sqlite3 + Litestream | LiteFS | LiteFS adds distributed read replicas. You don't need replicas at <50 CCU on one machine. Add later if you ever shard. |
| Drizzle | Prisma 7 | Prisma 7 (late 2025) finally dropped the Rust engine and is now competitive. Choose Prisma if a teammate prefers schema-first PSL and the Prisma Studio UI. Drizzle keeps the build simpler (no codegen) and is the tighter fit for `better-sqlite3`. |
| pnpm workspaces alone | pnpm + Turborepo | Add Turborepo when you have ≥5 packages, multiple build steps per package, or CI build times >2 min. For 3 packages (`shared`, `server`, `client`) it's pure overhead. Easy to bolt on later. |
| Vite | esbuild / Webpack / Rollup directly | Vite *is* esbuild + Rollup with sane defaults. No reason to drop to either layer manually unless you have a specific plugin need. |

---

## What NOT to Use

| Avoid | Why | Use Instead |
|-------|-----|-------------|
| **Socket.IO** | Bloated abstraction, opaque framing, ships with its own client lib that you have to load before connecting. Designed for the 2014 fallback-to-long-polling era; no benefit in a 2026 Chrome-only product. Adds latency vs raw WS. | Colyseus (which uses raw `ws`). |
| **uWebSockets.js for v1** | 2–10× faster than `ws` in benchmarks, but: (a) C++ binding, native build pain on Fly's musl/Alpine; (b) historically GitHub-only, npm presence is unofficial; (c) Colyseus 0.17 supports it as a transport but it's an optimization, not a default. At 50 CCU you have ~4 orders of magnitude of headroom. | Default `ws` (transitive via Colyseus). Switch only if profiling proves a bottleneck. |
| **Bun.serve** | "Stuck on Node" per project constraint, but worth naming: Bun's WS server is fast, but Better-Auth, `better-sqlite3`, Litestream, and Colyseus have varying levels of Bun maturity. Not the v1 risk to take. | Node 22 LTS. |
| **Lucia auth** | Officially deprecated March 2025 by maintainer. Future commits will be "learning resource" only. | Better-Auth. |
| **Plain `bcrypt` for new hashes** | OWASP 2026 ranks Argon2id #1; bcrypt is "legacy systems only" per the cheat sheet. Bcrypt remains *acceptable* but you have a clean slate, so pick the modern one. | `argon2` (Argon2id). Note: when migrating original BNO accounts, accept the old plaintext on first successful login and re-hash with argon2 transparently — see PITFALLS.md. |
| **Phaser 2 / CE** | EOL. Different API entirely. | Phaser 3.90 (or 4.x once ecosystem settles). |
| **UNet / old Unity Networking** | Already failed in `legacy/unity-project/`. Removed from Unity itself. | N/A — switching engines anyway. |
| **Photon (Exit Games)** | Vendor-locked, paid above small CCU thresholds, designed for client-hosted P2P-ish room model not server-authoritative. Was bundled-but-unused in the abandoned Unity port for a reason. | Colyseus. |
| **JSON over WebSocket** | Easy to debug but 2–10× larger than msgpack/schema, and you lose the discipline that produced the 39dll-style binary efficiency the original game shipped with. The original players noticed snappiness; a JSON server will feel worse. | `@colyseus/schema` for state, `msgpackr` for messages. |
| **Prisma + SQLite on Fly volumes** (combo) | Prisma's SQLite story has long been "works but isn't our priority"; Drizzle is purpose-built for sync drivers. | Drizzle + better-sqlite3. |
| **Postgres for v1** | Adds a network hop, a separate Fly app, connection-pool config, backup config, version-upgrade ops. Buys you nothing for a 50-CCU stateful server with single-writer access patterns. | better-sqlite3 + Litestream. Migrate to Postgres if/when CCU justifies it. |
| **`legacy/`-style plaintext credentials** | The original `localList.txt` of plaintext passwords is the single biggest historical lesson. Never re-implement. | Argon2id-hashed in SQLite, accessed via Better-Auth. |
| **`39dll`-style "the call order IS the protocol"** | The reason `decomp/wiki/08-39dll-networking.md` exists at all. No metadata, no length prefix, no version byte. Reverse-engineering the original is exactly this problem. | Schema-versioned binary frames via `@colyseus/schema`; explicit message types. |

---

## Stack Patterns by Variant

**If Phaser 3 proves a wrong fit during Stage 2 client analysis:**
- Drop to PixiJS 8.18.1 + `@pixi/tilemap` + `howler` (audio) + custom scene manager.
- Reason: GM5's room model is structurally close to Phaser scenes; if extracted features don't map (e.g., heavy custom-blit pipelines, non-standard collision), Phaser's batteries-included assumption hurts more than helps.
- Cost: ~2 weeks of glue work. Decision gate is documented in `PROJECT.md` as "decide end of Stage 2".

**If CCU target rises above ~500 mid-development:**
- Keep Colyseus, switch transport to `uWebSockets.js` via Colyseus's `WebSocketTransport` config.
- Add Fly machine autoscaling + Colyseus matchmaker driver (Redis).
- Migrate persistence to Fly Postgres + Drizzle (Drizzle's Postgres driver is identical API to its SQLite driver — no code rewrites in the data layer).

**If the original `.bno`/`.bnu`/`.bnb` formats turn out to be hard to walk programmatically:**
- Don't migrate them. Migrate only the account list (`localList.txt` → `users` table, argon2-hash on first login).
- Player inventory/state starts fresh; document this in the migration plan.

**If Fly.io regional latency disappoints (server in `iad`, players in EU/AU):**
- Single-region for v1 is fine at 50 CCU — the bottleneck is rarely transport.
- If needed: separate "rooms" per region (Colyseus matchmaker can route by `clientLatency`), one Fly machine per region, each with its own SQLite + Litestream replica to a shared Tigris bucket. Cross-region account read happens through the bucket, writes are sticky to home region.

---

## Version Compatibility

| Package A | Compatible With | Notes |
|-----------|-----------------|-------|
| colyseus@0.17.10 | @colyseus/schema@4.0.21, ws@8.20.0 | All transitively pinned by Colyseus. Don't manually upgrade `ws` past Colyseus's range. |
| colyseus.js (client) | colyseus@0.17.x server | Match minor versions exactly between client and server packages. |
| better-sqlite3@12.x | Node 22 LTS | Pre-built binaries published for Node 18/20/22. Builds from source on Alpine/musl (Fly default image) — use `flyctl`'s default `node` Dockerfile or the Node Buildpack. |
| drizzle-orm@0.45 + better-sqlite3@12 | drizzle-kit@latest | drizzle-kit handles migrations. Use `pnpm drizzle-kit push` for dev, generated SQL for prod. |
| better-auth@1.6 + drizzle | Drizzle adapter native | Better-Auth ships first-class Drizzle adapter; schema is generated by `npx @better-auth/cli generate`. |
| argon2@0.44 | Node 22 | Native binding. Pre-builds for linux-x64-musl available — important for Fly's default image. |
| phaser@3.90 + vite@8 | TypeScript 5.6+ | Phaser ships its own `.d.ts`; no `@types/phaser` needed. |
| msgpackr@1.11 | Browser + Node | Same package both halves. Use the `msgpackr/dist/index.mjs` ESM build in the client. |
| Litestream@0.3.x | better-sqlite3@12 | Litestream operates on the SQLite WAL file directly — driver-agnostic. Set `journal_mode=WAL` once on first open. |

---

## Fly.io Specifics

**Compute:** Single `fly.toml`-defined machine, `auto_stop_machines = "off"` (you want the WS server warm), `min_machines_running = 1`. Region: pick one close to majority player base — `iad` (Virginia) or `ord` (Chicago) for US-centric, `ams` for EU. Single region is correct for v1.

**Volume:** One `fly volumes create` for `/data` (10 GB is overkill but cheap). Mount in `fly.toml` under `[[mounts]]`. SQLite DB file lives at `/data/rebno.db`, Litestream WAL replicas in same volume before being shipped to Tigris.

**Object storage (Tigris):** `fly storage create` provisions a Tigris bucket and injects S3-compatible env vars (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT_URL_S3`, `BUCKET_NAME`). Use it for:
- Litestream replica target (`replicas: - type: s3` in `litestream.yml`).
- Static client asset CDN (sprites, audio) once you outgrow shipping them in the Vite bundle. Tigris is globally edge-cached so client load times stay snappy regardless of server region.

**WebSocket gotcha:** Fly proxies WS by default — no special config needed. But the Fly proxy applies a 60s idle-connection timeout; Colyseus's built-in ping (default 3s) keeps connections alive easily. Just make sure you don't disable Colyseus's `pingInterval`.

**Deploy:** `fly deploy` from `apps/server`. Use a multi-stage Dockerfile that runs `pnpm install --frozen-lockfile` for the workspace then `pnpm --filter server build`. Client (`apps/client`) deploys separately to a static host — either a second Fly app with `[http_service]`, or push the Vite `dist/` to Tigris and front with Fly's HTTPS edge.

**Postgres on Fly (only if you go that route):** `fly postgres create`, then `fly postgres attach` from the server app — injects `DATABASE_URL`. Drizzle's `pg` driver is a one-line swap from the SQLite driver. **Recommendation: don't.**

---

## Sources

- **npm registry** (`npm view <pkg> version`, 2026-05-01): authoritative current versions for all 14 packages above. **HIGH confidence.**
- [Colyseus docs](https://docs.colyseus.io/) — framework architecture, schema 4.x, transport options. **HIGH.**
- [@colyseus/schema npm](https://www.npmjs.com/package/@colyseus/schema) — version 4.0.21, decorator API. **HIGH.**
- [Colyseus + Phaser tutorial](https://docs.colyseus.io/learn) — confirms idiomatic pairing. **HIGH.**
- [Phaser 3.90 "Tsugumi" download](https://phaser.io/download/stable) and [v4.1.0 release notes](https://phaser.io/news/2026/04/phaser-4-1-0-salusa-release) — current versions, v4 release status. **HIGH.**
- [Phaser 3 → 4 Migration Guide](https://phaser.io/news/2026/04/migrating-from-phaser-3-to-phaser-4-what-you-need-to-know) — confirms v4 is "biggest release ever, migration mostly automatic" but still recent. **HIGH.**
- [Phaser vs PixiJS: Renderer vs Game Framework Comparison (2025)](https://generalistprogrammer.com/tutorials/phaser-vs-pixijs-renderer-comparison) — feature surface, performance, and "Phaser wins for tilemap-heavy games" conclusion. **MEDIUM** (single source, but well-aligned with Phaser's own docs).
- [PixiJS 8 + Create-PixiJS CLI](https://pixijs.io/create-pixi/docs/guide/installation/) — confirms Vite as a supported template option. **HIGH.**
- [Lucia Auth deprecation announcement](https://github.com/lucia-auth/lucia/discussions/1707) — official maintainer notice, March 2025 deprecation. **HIGH.**
- [Migrating from Lucia to Better Auth](https://www.nodejs-security.com/blog/nodejs-authentication-migration-from-lucia-to-better-auth) — Better-Auth as canonical replacement. **HIGH.**
- [Better-Auth changelog](https://better-auth.com/changelog) — current 1.6.9 features. **HIGH.**
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) — Argon2id #1, bcrypt legacy-only. **HIGH.**
- [Fly.io: All In on SQLite + Litestream](https://fly.io/blog/all-in-on-sqlite-litestream/) — official Fly endorsement of the pattern at this scale. **HIGH.**
- [Tigris Object Storage on Fly](https://fly.io/docs/tigris/) — provisioning + S3 compatibility. **HIGH.**
- [Drizzle vs Prisma 2026](https://www.bytebase.com/blog/drizzle-vs-prisma/) — current comparison; confirms Prisma 7's Rust-engine removal and Drizzle's edge for SQLite. **MEDIUM.**
- [pnpm + Turborepo monorepo guide](https://nhost.io/blog/how-we-configured-pnpm-and-turborepo-for-our-monorepo) — confirms pnpm-workspaces-alone is the lightweight path. **MEDIUM.**
- [WebSocket benchmarks (uWS vs ws, 2026)](https://piehost.com/websocket/best-websocket-libraries-benchmarks) — "2–10× faster" figure with caveat that `ws` is more than enough at <1K CCU. **MEDIUM.**
- [Geckos.io vs Colyseus thread (Phaser forum)](https://phaser.discourse.group/t/geckos-vs-colyseus/10383) — community consensus that WS+Colyseus is correct for non-twitch genres. **MEDIUM.**
- [msgpackr (npm)](https://www.npmjs.com/package/msgpackr) — fastest JS msgpack impl, browser+Node. **HIGH.**
- Project context: `C:\Users\decid\Documents\projects\rebno\.planning\PROJECT.md`, `.planning\codebase\STACK.md`, `.planning\codebase\CONCERNS.md`, `decomp\wiki\08-39dll-networking.md` — all read 2026-05-01.

---

*Stack research for: real-time multiplayer 2D web game on Node + TS + Fly.io*
*Researched: 2026-05-01*
