The Last of the Snacks

Trust no one. Season aggressively.

End-to-end product: lobby, rooms, authoritative game server, validated messages, i18n (EN/ES), and production-style deploy (nginx/Traefik, blue/green). Shows full-stack ownership beyond CRUD APIs.

Overview

A browser-based, turn-based hidden-role card game for 4–8 players. Each session runs in a room with a dedicated game loop on the server: lobby configuration, turn timers, card resolution, spectators, and optional bots. The frontend is a polished Next.js experience; the backend is an authoritative Node server so clients never decide outcomes alone—exactly the split you want when interviewing for real-time or multiplayer systems.

Architecture

Monorepo (npm workspaces): @last-of-snack/shared (types and domain constants) is built first and consumed by both apps. The game server (Fastify + WebSocket) owns rooms and state in memory. The frontend (Next.js 14 App Router) drives UI, client state (Zustand), and can proxy HTTP to the game server for room creation in dev or behind nginx in prod.

Real-time flow & validation

  • Connection: Clients open a WebSocket to the game server (/ws); messages drive join, lobby settings, chat, start game, play card, restart, etc.
  • Authoritative state: GameEngine, turn system, card resolution, and timers run on the server; broadcasts keep every client in sync (including spectators).
  • Schema-first protocol: Incoming payloads are validated with Zod before touching game logic—reduces abuse and makes the contract explicit for future features or mobile clients.
  • HTTP edge: Room creation can go through Next.js /api/rooms (proxy) or directly to the game server depending on environment—same pattern as many production BFF setups.

System design

flowchart TB
  subgraph client [Browser]
    UI[Next.js 14 App Router<br/>React + Zustand]
  end

  subgraph monorepo [npm workspaces monorepo]
    SHARED["@last-of-snack/shared<br/>types, domain constants"]
    FE[frontend]
    GS[game-server]
    SHARED --> FE
    SHARED --> GS
  end

  UI -->|HTTP: pages, assets| FE
  UI -->|HTTP: create room<br/>/api/rooms proxy| FE
  FE -->|HTTP: rooms API| GS
  UI -->|WebSocket /ws<br/>game messages| GS

  subgraph gs_internal [game-server internals]
    WS[Fastify + @fastify/websocket]
    ZOD[Zod message validation]
    RM[RoomManager / rooms]
    GE[GameEngine, turns, timers, cards]
    WS --> ZOD --> RM --> GE
  end

  GS --> WS

Delivery & operations

Docker Compose runs frontend + game server + nginx locally; production paths include Traefik and a documented blue/green upgrade flow for zero-downtime swaps. Environment variables separate public client config from server-only secrets—familiar production hygiene.

Tech stack

Next.js 14 (App Router) React 18 TypeScript (strict) Zustand Fastify WebSockets (@fastify/websocket) Zod npm workspaces Docker / Docker Compose nginx / Traefik Blue/green deploy i18n (EN/ES) CSS Modules