# Tech Stack — Deadhead: Space Freight Tycoon Companion to `game-design-document.md`. ## TL;DR **TypeScript end to end, in one monorepo.** | Layer | Choice | |---|---| | Language | TypeScript (client, server, shared sim) | | Monorepo | pnpm workspaces | | Client UI | React + Vite | | Client state | Zustand (game state) + TanStack Query (server sync) | | System map rendering | PixiJS (via `@pixi/react`) | | Styling | Tailwind CSS | | PWA | `vite-plugin-pwa` (Workbox) | | Native wrapper | Capacitor (iOS/Android) | | API server | Node + Fastify | | Database | PostgreSQL | | ORM / migrations | Drizzle ORM | | Validation | Zod (shared schemas, client + server) | | Auth | Better Auth (self-hosted) — or Supabase Auth if going managed | | Job queue / scheduler | pg-boss (Postgres-backed, no Redis needed) | | Push notifications | `web-push` (VAPID) for PWA; FCM/APNs via Capacitor for native | | Planetary positions | `astronomy-engine` | | Seeded RNG | Small pure-TS PRNG (e.g., sfc32 / mulberry32) | | Testing | Vitest (unit/sim), Playwright (E2E) | | Deployment | Docker containers (API + Postgres), static client on a CDN | --- ## 1. Why TypeScript Everywhere The single most important architectural decision is the **deterministic dispatch model** (GDD §14): a trip's arrival time, position, and events are all derived from its dispatch record and RNG seed. That logic has to run in **two places**: - **Client** — to show ships moving along their routes, display ETAs, and preview a contract before dispatch. - **Server** — as the authority that resolves payouts, XP, and events. If both sides run the *same code*, they can never disagree. A shared TypeScript package gives you that for free. With a C# or Python backend you'd be maintaining two implementations of the sim and chasing drift bugs forever. ## 2. Repo Layout ``` /apps /web React + Vite client (also the Capacitor app) /server Fastify API, workers /packages /sim Pure, deterministic game logic (no I/O) /schema Zod schemas + shared types (API contracts) /content Static game data: ships, destinations, goods, skills ``` ### `packages/sim` rules - **Pure functions only.** No `Date.now()`, no `Math.random()`, no network, no DB. - Time is always passed in as an argument. - Randomness comes only from a seeded PRNG derived from the dispatch seed. - Fully unit-testable: given a dispatch record + timestamp, the output is always identical. Core functions (illustrative): ```ts travelTime(route, ship, cargo, throttle): Duration positionAt(dispatch, now): Vec2 rollEvents(dispatch): GameEvent[] // pre-rolled at dispatch time resolveDispatch(dispatch, now): Resolution // payout, XP, damage, discovery generateContracts(bay, worldState, seed): Contract[] ``` ### `packages/content` Ships, destinations, goods, and skills live as typed data files (TS or JSON validated by Zod). This makes balancing a data edit, not a code change, and makes the content easy to hot-reload during tuning. ## 3. Client ### React + Vite The game is overwhelmingly **UI**: contract boards, fleet lists, pilot sheets, bay management. That's what React is good at. A game engine would be fighting you on every menu. ### PixiJS for the system map The one truly "game-like" view is the solar system map with ships moving along routes. PixiJS gives GPU-accelerated 2D rendering that stays smooth on phones, embedded inside the React app as a single component. - Planets positioned from `astronomy-engine` (scaled/compressed for readability — soft sci-fi). - Ships interpolated along routes with `sim.positionAt()` every frame. - Pan/zoom with touch support. ### State - **TanStack Query** owns server data (fleet, contracts, company) — caching, refetch on focus, optimistic updates on dispatch. - **Zustand** for local UI and derived real-time state (current clock, selected ship, map camera). ### Server time on the client On connect, the client fetches server time and stores the offset. All "now" values used for rendering come from `Date.now() + serverOffset`, so a wrong device clock only affects visuals and is never trusted for outcomes. ### PWA + Capacitor - `vite-plugin-pwa` makes the web build installable with offline asset caching. - Capacitor wraps the **same build** for the App Store and Play Store when you want that. It gives more reliable push notifications and native polish without a second codebase. - Web Push works on installed PWAs on iOS 16.4+, so Capacitor can wait until after launch. ## 4. Server ### Fastify Fast, TypeScript-friendly, schema-based validation (pairs well with Zod via `fastify-type-provider-zod`). Small surface area. ### API shape Plain REST/JSON is enough. This isn't a twitch-multiplayer game; nothing needs WebSockets at launch. The client polls/refetches on focus, which is exactly the "pick up the phone, see what happened" pattern. ### Server-authoritative resolution - **Dispatch:** server validates (ship available, pilot qualified, landing class OK, fuel range OK), stamps departure time, generates the seed, pre-rolls events via `sim`, and stores the record. - **Resolution:** happens lazily when the player loads state *and* via a scheduled job at arrival time (so notifications fire and the economy stays consistent even if the player never opens the app). - Resolution must be **idempotent**: a dispatch resolves exactly once, guarded by a status column + transaction. ### PostgreSQL + Drizzle - Relational data fits this game well: companies, ships, pilots, bays, contracts, dispatches. - Drizzle is TypeScript-first, light, and its schema can live in `packages/schema` alongside Zod types. - JSONB columns for flexible bits (pre-rolled event lists, ship loadouts). ### pg-boss for scheduled work Postgres-backed job queue, so you don't need to run Redis. - Schedule an "arrival" job per dispatch → resolve + send push notification. - Periodic jobs: regenerate contract boards, recompute fuel prices, run automation rules (standing routes). ### Auth - **Better Auth**: self-hosted, TypeScript-native, supports email/password, OAuth (Google/Apple/Discord), and anonymous accounts. - Strongly recommend **guest accounts upgradable to full accounts** — players can start instantly on mobile and link an account later to sync across devices. - If you'd rather not run auth yourself, Supabase (Auth + Postgres) is the managed alternative and plays fine with Drizzle. ## 5. Notifications | Platform | Mechanism | |---|---| | Web / installed PWA | Web Push with VAPID keys (`web-push` library) | | Capacitor iOS | APNs via `@capacitor/push-notifications` | | Capacitor Android | FCM via `@capacitor/push-notifications` | Fired from the pg-boss arrival job. Batch notifications when many ships arrive at once ("4 ships arrived — +12,400 cr"). ## 6. Planetary Positions `astronomy-engine` computes real planetary positions for any date, in pure JS, no API calls. - Run it in `packages/sim` so client and server agree on distances. - Use a fixed **game epoch** mapped to real ephemeris dates, advanced by the compressed game clock (1 real min = 1 game hour by default). - Moons and stations inherit their parent body's position plus a small fixed offset — soft sci-fi doesn't need lunar orbits. - For interstellar (late game), real star positions from a static dataset in `packages/content`. ## 7. Deployment - **Client:** static build on any CDN (Cloudflare Pages, Netlify, Vercel). - **Server + Postgres + workers:** Docker containers. Easy to run on a home Docker host during development, then move to a VPS or a managed platform (Fly.io, Railway, Render) for production. - **Backups:** automated Postgres dumps from day one — player saves *are* the database. ## 8. Testing - **Vitest on `packages/sim`** is where most of the value is. Determinism tests: same dispatch + time → same result, every time. - **Balance simulations:** scripts that run thousands of simulated dispatches through `sim` to check payout curves and progression pacing without playing the game. - **Playwright** for a handful of critical E2E flows (sign up → first job → first ship). ## 9. Alternatives Considered | Option | Why not (for this game) | |---|---| | Unity | Heavy web builds, slow mobile-web load times, painful for UI-heavy screens. | | Godot | Better than Unity for web, but still a game engine for a game that's mostly menus; larger downloads than a React app. | | Phaser | Great for action games; the map is the only part that needs it, and PixiJS covers that more lightly. | | .NET backend | Solid, but loses shared sim code with the client (unless you go all-in on Blazor, which is a weaker mobile web story). | | Firebase | Fast to start, but deterministic, transactional server-side resolution is awkward in Firestore + Cloud Functions. | | Redis + BullMQ | Excellent, but an extra service to run when Postgres + pg-boss covers the need at this scale. | ## 10. Build Order 1. `packages/sim` + `packages/content`: travel time, positions, seeded events, resolution. Fully tested, no UI. 2. Server: auth (guest accounts), dispatch + resolve endpoints, Postgres schema. 3. Client: contract board, fleet list, dispatch flow. Plain UI, no map yet. 4. pg-boss arrival jobs + Web Push. 5. PixiJS system map. 6. PWA install, then Capacitor builds.