Skip to content

Shared Packages Architecture

Status: Partially implementedpackages/types created as part of TypeScript Typing Improvements. Remaining packages deferred.

Why

Types and contracts that cross app boundaries (server ↔ client ↔ admin) are currently defined in the server and either duplicated or left as any in the other apps. This means:

  • Renaming a field in GameState requires hunting across three apps
  • The client has no guaranteed type for socket event payloads
  • The admin app re-invents types it already knows from the server

The fix is a set of small, zero-runtime packages under packages/ that are installed as local workspace dependencies. Each app imports from them; types are defined once.


Package Map

packages/types — Core domain types

What goes here: Pure TypeScript interfaces for the game domain. No framework code, no runtime logic.

TypeDescription
GameStateFull in-memory game state
GameMoveA single move record
GameMoveData / PlayBandDataMove payload shapes
BandEntryA played band in the bands collection
TurnTimerActive turn timer state
GamePlayerPlayer identity within a game

Consumers: server, client, admin

Status: Created — added in TypeScript Typing Improvements phase.


packages/api-contracts — Socket & HTTP payload types

What goes here: The exact shape of every socket event and REST request/response. Both server and client import from here, so a payload change is a compile error on both sides simultaneously.

Socket events (client → server):

EventPayload type
makeMove{ gameId: string; playerId: string; moveType: string; moveData: GameMoveData }
joinGame{ gameId: string; playerId: string }
revealBand{ gameId: string; bandId: string; playerId: string }
abandonGame{ gameId: string; playerId: string }
clientTimeout{ gameId: string; playerId: string }
playerTyping{ gameId: string; playerId: string; isTyping: boolean }

Socket events (server → client):

EventPayload type
gameStateGameState
gameBandsBandEntry[]
moveCompleted{ playerId: string; moveType: string; moveData: GameMoveData; timestamp: Date }
moveError{ error?: string; illegal?: boolean; reason?: string; gameState?: GameState }
timerStarted{ playerId: string; turnStartedAt: Date; maxTurnTime: number }
timerCleared{ playerId: string }
playerJoined{ playerId: string }
gameAbandoned{ playerId: string }
opponentTyping{ gameId: string; playerId: string; isTyping: boolean }
turnTimeout{ playerId: string }

Depends on: packages/types

Consumers: server, client, admin

Status: Deferred — implement when the client is being typed up.


packages/db-schema — Database layer types

What goes here: Drizzle ORM schema definitions and Firestore raw document shapes. Only the server touches this.

ExportDescription
RawGameStateDocFirestore on-disk shape before timestamp conversion
Drizzle table definitionsCurrently scattered in database.service.ts area

Consumers: server only

Status: Deferred — implement when consolidating the database layer. Low urgency; server-internal.


packages/ui-types — Client view model types

What goes here: Display-layer types that don't map 1:1 to domain types — things like filtered/masked views, component prop shapes, and client-side derived state.

Examples:

  • HiddenBandEntryBandEntry with bandName: null (opponent's unrevealed band)
  • MatchSummary — condensed match result for history lists

Consumers: client, admin

Status: Deferred — implement when the client is being typed up alongside api-contracts.


Implementation Order

When the time comes to implement a new package:

  1. Create packages/<name>/package.json:
json
{
  "name": "@band-game/<name>",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  }
}
  1. Create packages/<name>/tsconfig.json:
json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src"]
}
  1. Create packages/<name>/src/index.ts — barrel export of all types.

  2. Add to consuming app's package.json:

json
"dependencies": {
  "@band-game/<name>": "workspace:*"
}
  1. Run pnpm install to link the workspace package.

Notes

  • These packages are type-only — no runtime code, no build step needed. Consuming apps include the source directly via TypeScript's path resolution.
  • The root tsconfig.json should have paths entries if strict module resolution is needed; otherwise workspace:* + "main": "./src/index.ts" is sufficient for NestJS + Vite.
  • packages/db-schema may eventually absorb the Drizzle schema from the server, enabling type-safe query results to be shared without coupling the client to the ORM.