Shared Packages Architecture
Status: Partially implemented — packages/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
GameStaterequires 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.
| Type | Description |
|---|---|
GameState | Full in-memory game state |
GameMove | A single move record |
GameMoveData / PlayBandData | Move payload shapes |
BandEntry | A played band in the bands collection |
TurnTimer | Active turn timer state |
GamePlayer | Player 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):
| Event | Payload 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):
| Event | Payload type |
|---|---|
gameState | GameState |
gameBands | BandEntry[] |
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.
| Export | Description |
|---|---|
RawGameStateDoc | Firestore on-disk shape before timestamp conversion |
| Drizzle table definitions | Currently 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:
HiddenBandEntry—BandEntrywithbandName: 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:
- 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"
}
}- Create
packages/<name>/tsconfig.json:
json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src"]
}Create
packages/<name>/src/index.ts— barrel export of all types.Add to consuming app's
package.json:
json
"dependencies": {
"@band-game/<name>": "workspace:*"
}- Run
pnpm installto 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.jsonshould havepathsentries if strict module resolution is needed; otherwiseworkspace:*+"main": "./src/index.ts"is sufficient for NestJS + Vite. packages/db-schemamay eventually absorb the Drizzle schema from the server, enabling type-safe query results to be shared without coupling the client to the ORM.