The poker engine uses npm workspaces to manage multiple packages in a single repository. This document describes the workspace layout, inter-package dependencies, and development workflows.
Workspace Layout
poker-engine/ # Root workspace
├── package.json # Root — defines workspaces, shared scripts
├── src/ # Backend (Node.js / Express / Socket.io)
├── client/ # Web frontend (React / Vite)
├── packages/
│ ├── shared/ # @poker/shared — cross-platform types, stores, hooks, utils
│ ├── mobile/ # @poker/mobile — React Native app (Expo)
│ └── vendor/ # Local dependency replacements (not workspaces)
├── prisma/ # Database schema and migrations
├── docs/ # Documentation
└── scripts/ # Build, deployment, Ralph automation scripts
Root package.json workspaces configuration:
{
"name": "poker-engine",
"workspaces": [
"packages/shared",
"packages/mobile",
"client"
]
}
The backend (src/) is not a separate workspace — it lives in the root package.
packages/shared — @poker/shared
The shared package provides cross-platform types, Zustand stores, hooks, i18n setup, utilities, and storage abstractions used by the web client and the mobile app. The backend also imports types from it.
Package Identity
{
"name": "@poker/shared",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts",
"default": "./src/index.ts"
}
}
}
The exports point directly to TypeScript source — Vite resolves from source, so there is no build step needed when developing the web client or mobile app.
Directory Structure
packages/shared/src/
├── index.ts # Root barrel — re-exports all public API
├── types/
│ ├── index.ts # Types barrel
│ ├── events.ts # WebSocket event types (ServerToClientEvents, ClientToServerEvents)
│ ├── game.ts # Game state types (TableState, TablePlayer, Card, GameType)
│ ├── api.ts # API types (ApiResponse, AuthTokens, LoginResult)
│ ├── models.ts # Data models (User, Tournament, FeatureToggles, UserSettings)
│ └── rules.ts # Rules types
├── hooks/
│ ├── index.ts # Hooks barrel
│ ├── gameStateHelpers.ts
│ ├── useHandTransition.ts # Hand-to-hand transition timeline
│ └── useSequenceTracker.ts
├── i18n/
│ └── index.ts # Shared i18next setup
├── utils/
│ ├── index.ts # Utils barrel
│ ├── auth-lifecycle.ts
│ ├── currency.ts # Currency formatting (platform-agnostic)
│ └── cards.ts # Card display utilities (suits, ranks, symbols)
├── storage/
│ ├── index.ts # StorageAdapter interface
│ ├── web.ts # createWebStorageAdapter() — wraps localStorage
│ ├── native.ts # createNativeStorageAdapter() — stub
│ └── zustand-storage.ts # createZustandStorage() — StorageAdapter → Zustand StateStorage
└── stores/
├── index.ts # Stores barrel
├── toastStore.ts # Tier 1: simple store, copy-as-is
├── tableSessionStore.ts # Tier 1: simple store
├── settingsStore.ts # Tier 1: simple store
├── wageringProgressStore.ts # Tier 1: simple store
├── lobbyStore.ts # Tier 2: uses initLobbyStorage(adapter)
├── gameStore.ts # Tier 2: uses initGameStorage(adapter)
├── authStore.ts # Tier 3: factory createAuthStore(config)
├── i18nStore.ts # Tier 3: factory createI18nStore(config)
└── userSettingsStore.ts # Tier 3: factory createUserSettingsStore(config)
Store Tiers
Stores are categorized by their dependency complexity:
| Tier | Pattern | When to Use |
|---|---|---|
| Tier 1 | create<State>()(...) — direct export | No persistence, no platform APIs |
| Tier 2 | initXStorage(adapter) + useXStore | Needs localStorage/AsyncStorage |
| Tier 3 | createXStore(config) factory | Needs persist middleware, callbacks, or API client |
See packages/shared/AGENTS.md for detailed examples of each tier.
Dependencies
{
"dependencies": {
"i18next": "25.8.10",
"react-i18next": "16.5.4",
"zustand": "5.0.0"
},
"peerDependencies": {
"react": ">=18" // Optional — not needed for non-React consumers
}
}
Key Conventions
- ESM imports require
.jsextension:from './types/events.js'notfrom './types/events'(NodeNext module resolution) - Utilities must be pure: No store imports, no browser/platform APIs. Accept configuration as parameters
- No duplicate barrel exports: If a type is already exported via
types/index.ts, don’t re-export it from a store file composite: truein tsconfig: Enables project references for workspace-level typecheck
packages/mobile — @poker/mobile
The React Native app built with Expo. Targets iOS and Android.
Package Identity
{
"name": "@poker/mobile",
"version": "0.0.1",
"main": "expo-router/entry"
}
Key Dependencies
| Package | Purpose |
|---|---|
expo | React Native build toolchain |
expo-router | File-based navigation |
@poker/shared | Shared types, stores, utilities |
@react-native-async-storage/async-storage | Native storage (implements StorageAdapter) |
socket.io-client | WebSocket connection to backend |
nativewind | Tailwind CSS for React Native |
expo-notifications | Push notifications |
expo-local-authentication | Biometric auth |
zustand | State management |
Build & Distribution Scripts
# Development
npm run start # Start Expo dev server
npm run ios # Run on iOS simulator
npm run android # Run on Android emulator
# Detox end-to-end tests
npm run test:e2e:ios
npm run test:e2e:android
# EAS builds (requires Expo account)
npm run build:dev # Development build (all platforms)
npm run build:preview # Preview/staging build
npm run build:production # Production build
# App store submission
npm run submit:ios # Build + submit to App Store
npm run submit:android # Build + submit to Play Store
# Both also have :build-only and :submit-only variants
Storage Adapter Integration
createNativeStorageAdapter() in packages/shared/src/storage/native.ts is a stub that throws. The mobile app provides the concrete StorageAdapter implementation in packages/mobile/src/lib/storage.ts, using @react-native-async-storage/async-storage.
client/ — Web Frontend
The React web application built with Vite. It is an npm workspace with its own package.json (package name poker-engine-client).
Referencing @poker/shared
The client imports from @poker/shared using the package name. Vite resolves this to packages/shared/src/index.ts via the npm workspace symlink — no build step required:
import { useAuthStore, type TableState } from '@poker/shared';
Frontend-Specific Constraints
When writing code for the frontend or shared package (used in the frontend):
- Do not use
NodeJS.*types (e.g.,NodeJS.Timeout) —@types/nodeis not inclient/package.json - Use
ReturnType<typeof setTimeout>instead ofNodeJS.Timeout - Do not use
process.envdirectly — access configuration through the store or API
Cross-Package Dependencies
Backend (src/)
└─ Prisma client (@prisma/client)
└─ @poker/shared (types)
└─ No dependency on client
Web Client (client/)
└─ @poker/shared (workspace link → packages/shared/src/)
└─ No dependency on backend source (communicates via HTTP/WebSocket)
Mobile (@poker/mobile)
└─ @poker/shared (workspace link → packages/shared/src/)
└─ No dependency on backend source (communicates via HTTP/WebSocket)
@poker/shared
└─ zustand, i18next, react-i18next
└─ No dependency on client, mobile, or backend
The shared package is a true leaf — it has no dependencies on other workspace packages.
Running the Full Stack
# Install all workspace dependencies (from root)
npm install
# Run backend and frontend in separate terminals (development)
npm run dev # Backend only (tsx watch)
npm run dev:client # Frontend only (Vite dev server)
# Or start both in the background
./scripts/game.sh start
# Run mobile app
cd packages/mobile && npm run start
Typechecking Across Workspaces
# Everything: backend (src), scripts, and every workspace
npm run typecheck
# Backend only
npm run typecheck:src
# Scripts only
npm run typecheck:scripts
# One workspace
npm run typecheck --prefix client
npm run typecheck --prefix packages/shared
npm run typecheck --prefix packages/mobile
The test and release workflows both run npm run typecheck.
Adding a New Shared Type
- Add to the appropriate file in
packages/shared/src/types/(e.g.,game.ts) - Export from
packages/shared/src/types/index.ts - The type is automatically available in both the web client and mobile app via
@poker/shared
Example:
// packages/shared/src/types/game.ts
export interface MyNewType {
id: string;
value: number;
}
// packages/shared/src/types/index.ts
export * from './game.js'; // already included — no change needed
Adding a New Shared Utility
- Create
packages/shared/src/utils/my-util.ts - Export pure functions (no side effects, no store imports)
- Add
export * from './my-util.js'topackages/shared/src/utils/index.ts
// packages/shared/src/utils/my-util.ts
export function formatSomething(value: number, precision: number): string {
return value.toFixed(precision);
}
Gotchas
- ESM
.jsextensions required in all shared package imports (even for.tssource files) — NodeNext module resolution requires this - Exports map to source — no build step needed for web/mobile development; only needed for distribution
- Storage adapters initialized before React — call
initLobbyStorage()andinitGameStorage()inmain.tsxbeforecreateRoot(), not inside components composite: truein tsconfig — required for project references to work across workspace packagesreactis an optional peer dep in shared — allows non-React consumers (e.g., future CLI tools) to import types without installing React