# Architecture Living reference for how the code is organised. Update it after any database schema or API change (implementation plan rule 0.1.9). Decisions and their reasons live in `DECISIONS.md`. ## Packages | Package | Role | Depends on | |---|---|---| | `packages/content` | Static game data (locations, ship models, cargo, skills, `GameConfig`) as Zod-validated TypeScript, plus the `content` bundle with id lookups. | zod | | `packages/sim` | Pure, deterministic game rules: clock, seeded RNG, positions, distances, travel, fuel, eligibility, contract boards, dispatch validation/records, flight progress, XP, resolution. No I/O, no `Date.now()`, no `Math.random()`. | content, astronomy-engine | | `packages/schema` | Zod schemas for every API request, response and error. Shared by server and client. | sim (error codes), zod | | `apps/server` | Fastify API, Postgres via Drizzle, pg-boss workers. The authority on outcomes. | content, sim, schema | | `apps/web` | React client. Predicts with sim, never decides outcomes. | content, sim, schema | ## Conventions - **Times** are Unix milliseconds (numbers). A *real* time is a wall-clock instant; a *game* time is the instant of the in-game date (`realToGame` / `gameToReal` in sim). - **Ids** are non-empty strings. Content ids are kebab-case (`earth`, `bulk-hauler`). - **Credits** are whole numbers. - **Static vs. player data:** the API never sends static data. The client imports `@deadhead/content` and joins by id. ## API data model (`packages/schema`) | Shape | Fields | |---|---| | `ServerTime` | `realTime`, `gameTime` | | `Company` | `id`, `name`, `credits` | | `Ship` | `id`, `modelId`, `name`, `status` (`idle` / `in-flight`), `locationId` (null in flight), `dispatchId` (null when idle), `pilotId` | | `Pilot` | `id`, `name`, `isAvatar`, `xp`, `level`, `skills` (`{ skillId: level }`), `skillPointsAvailable`, `assignedShipId`, `status`, `locationId` (null in flight) | | `Bay` | `id`, `locationId` | | `LocationState` | `id`, `discovered` | | `Contract` | mirrors sim `Contract` | | `Dispatch` | mirrors sim `DispatchRecord` + `status` (`in-flight` / `resolved`), `resolvedAt` | | `ResolutionSummary` | `dispatchId`, `shipId`, `pilotId`, `originId`, `destinationId`, `tutorial`, `credits`, `pilotXp`, `discoveredLocationId`, `resolvedAt` | | `TutorialState` | `loanerLocationId` (`earth` / `moon`), `runInFlight` | | `PlayerState` | `company`, `ships`, `pilots`, `bays`, `locations`, `tutorial` (null once over), `activeDispatches`, `summaries` | ### Errors Every non-2xx response has the body `{ error: { code, message } }`. `code` is one of `ERROR_CODES`: every sim dispatch and reposition validation reason (`DISPATCH_FAILURES`, `REPOSITION_FAILURES`), unchanged, plus the server codes in `SERVER_ERROR_CODES` (`validation-error`, `unauthorized`, `not-found`, `insufficient-funds`, ...). ## API routes (`packages/schema/src/routes.ts`) The `routes` catalog is the single source for both server and client. Each entry has `method`, `path`, `request` (path parameters merged with the body), `response` (2xx body) and `auth`. Errors always use the error body above. | Name | Method & path | Request | Response | Auth | |---|---|---|---|---| | `getServerTime` | `GET /api/time` | — | `ServerTime` | no | | `createGuest` | `POST /api/guest` | — | `{ playerId, state: PlayerState }` + session cookie | no | | `getPlayerState` | `GET /api/state` | — | `PlayerState` | yes | | `getContractBoard` | `GET /api/bays/:locationId/board` | `locationId` | `{ contracts, periodEndsAt }` | yes | | `getTutorialBoard` | `GET /api/tutorial/board` | — | `{ contracts, periodEndsAt }` | yes | | `createDispatch` | `POST /api/dispatches` | `{ kind: "contract", contractId, shipId, pilotId }` / `{ kind: "tutorial", contractId }` / `{ kind: "reposition", shipId, pilotId, destinationId }` | `{ dispatch }` | yes | | `buyShip` | `POST /api/ships` | `modelId` | `{ ship }` | yes | | `leaseBay` | `POST /api/bays` | `locationId` | `{ bay }` | yes | | `hirePilot` | `POST /api/pilots` | `locationId` (a player bay) | `{ pilot }` | yes | | `assignPilot` | `POST /api/pilots/:pilotId/assign` | `pilotId`, `shipId` | `{ pilot }` | yes | | `unassignPilot` | `POST /api/pilots/:pilotId/unassign` | `pilotId` | `{ pilot }` | yes | | `spendSkillPoint` | `POST /api/pilots/:pilotId/skills` | `pilotId`, `skillId` | `{ pilot }` | yes | | `acknowledgeSummaries` | `POST /api/summaries/acknowledge` | `dispatchIds` | `{ acknowledged }` | yes |