Monorepo Layout

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:

TierPatternWhen to Use
Tier 1create<State>()(...) — direct exportNo persistence, no platform APIs
Tier 2initXStorage(adapter) + useXStoreNeeds localStorage/AsyncStorage
Tier 3createXStore(config) factoryNeeds 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 .js extension: from './types/events.js' not from './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: true in 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

PackagePurpose
expoReact Native build toolchain
expo-routerFile-based navigation
@poker/sharedShared types, stores, utilities
@react-native-async-storage/async-storageNative storage (implements StorageAdapter)
socket.io-clientWebSocket connection to backend
nativewindTailwind CSS for React Native
expo-notificationsPush notifications
expo-local-authenticationBiometric auth
zustandState 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/node is not in client/package.json
  • Use ReturnType<typeof setTimeout> instead of NodeJS.Timeout
  • Do not use process.env directly — 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

  1. Add to the appropriate file in packages/shared/src/types/ (e.g., game.ts)
  2. Export from packages/shared/src/types/index.ts
  3. 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

  1. Create packages/shared/src/utils/my-util.ts
  2. Export pure functions (no side effects, no store imports)
  3. Add export * from './my-util.js' to packages/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 .js extensions required in all shared package imports (even for .ts source 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() and initGameStorage() in main.tsx before createRoot(), not inside components
  • composite: true in tsconfig — required for project references to work across workspace packages
  • react is an optional peer dep in shared — allows non-React consumers (e.g., future CLI tools) to import types without installing React
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.