A Jackbox-style multiplayer game framework built with Next.js
This project is a modular party game engine that lets developers quickly build simple multiplayer games that friends can play together by joining a shared room on their phones.
It uses a plugin-style architecture:
- The engine manages rooms, players, actions, state transitions, and syncing.
- Each game defines its own reducer, state machine, and UI.
- Games can be added by simply creating a folder and registering it — no changes to core engine files.
This design makes the system easy to extend, fast to iterate, and ideal for building lots of small party-style games.
The project is built using Next.js App Router with a clean separation between:
Handles everything related to the game simulation:
- Room creation (
/api/create-room) - Joining rooms (
/api/join-room) - Polling room & game state (
/api/get-room) - Dispatching player actions (
/api/game-action) - Running reducers for each game
- Validating allowed actions
- Keeping room state in an in-memory store (dev-friendly)
The engine is game-agnostic — it does not know anything about UI or phases of any specific game.
Each game defines:
initialState(players)reducer(state, action, ctx)getPhase(state)isActionAllowed(...)
A game has full control over its state machine — phases, rules, winning logic, etc.
All games conform to the same template, so the engine can run any game without modification.
Each game provides a GameView component that receives:
{
room,
state, // gameState
playerId,
isHost,
dispatchAction
}This lets each game:
- Show its own UI
- Send arbitrary actions to the server via
dispatchAction - Render differently based on host vs. player role
- Use any internal UX patterns without affecting other games
RoomPage is fully generic:
- It loads room + gameState via polling
- Detects if the user is the host
- Looks up the active game in:
gameRegistry(logic)views.ts(UI)
- Renders:
- Shared chrome (players, header)
- Game-specific view component
The room page never needs to be updated when new games are added.
All games follow the same lifecycle:
- Host chooses a game on the Create Room page.
- Room is created with:
- Unique 4-letter code
- Selected game's ID
- Host identity
- Players join using the room code.
- Game reducers drive all state changes.
- The engine stores and syncs the updated state.
- Views re-render using the new gameState.
This is where the engine shines.
To add a new game:
Example:
/src/games/trivia-blitz/
/src/games/trivia-blitz/config.ts
This file defines:
export const triviaBlitzGame = defineGame({
id: "trivia-blitz",
name: "Trivia Blitz",
minPlayers: 2,
maxPlayers: 10,
initialState(players) { ... },
reducer(state, action, ctx) { ... },
getPhase(state) { return state.phase; },
isActionAllowed() { ... }
});All game logic lives here.
Create:
/src/games/trivia-blitz/GameView.tsx
The component receives shared game props:
export type GameViewProps = {
room;
state;
playerId;
isHost;
dispatchAction(type, payload);
};Use dispatchAction('ACTION_NAME', payload) to send actions to the server.
Add to /src/engine/gameRegistry.ts:
import { triviaBlitzGame } from "@/games/trivia-blitz/config";
games.set(triviaBlitzGame.id, triviaBlitzGame);And to /src/games/views.ts:
import { TriviaBlitzGameView } from "./trivia-blitz/GameView";
// Add to gameOptions array
export const gameOptions: GameOption[] = [
// ... existing games
{
id: "trivia-blitz",
name: "Trivia Blitz",
description: "Fast-paced trivia battles!",
minPlayers: 2,
maxPlayers: 10,
},
];
// Add to gameViews registry
const gameViews: Record<string, GameViewComponent> = {
// ... existing games
"trivia-blitz": TriviaBlitzGameView as GameViewComponent,
};That's it.
One deploy → new game is live.
npm install
npm run devAccess on your phone:
- Find your machine's LAN IP (e.g.
192.168.1.12) - Visit:
http://192.168.1.12:3000
This is the best way to test multi-device behavior.
Rooms and game state are stored in an in-memory Map.
- Works great locally.
- Fine for short sessions on Vercel.
- Rooms reset when serverless instances restart or redeploy.
Future improvement: plug in Redis or a KV store for persistence.
Every push to main redeploys to Vercel.
You do not need a new project for each game — all games live inside one engine and deploy together.
- Fastest possible iteration loop
- No backend changes needed to add new games
- Reasonable for tiny prototypes, but strong enough for a real product
- Clean separation:
- Server handles logic
- Client handles UI
- Reducers enforce rules
- Flexible: turn-based games, trivia, voting games, bluffing games — all follow the same pattern
- Replace in-memory store with Redis/KV
- Add WebSockets for real-time sync
- Add animations or transitions per-game
- Create reusable game components (timers, buzzers, modals)
- Add a Game Browser homepage
src/
├── app/ # Next.js App Router pages
│ ├── api/ # API routes (create-room, join-room, etc.)
│ ├── create/ # Create room page
│ ├── join/ # Join room page
│ └── rooms/[roomCode]/ # Generic room page
├── engine/ # Core game engine
│ ├── types.ts # Type definitions
│ ├── stateStore.ts # In-memory room storage
│ ├── gameRegistry.ts # Game template registry
│ ├── applyActionToRoom.ts # Action dispatcher
│ └── defineGame.ts # Helper for defining games
├── games/ # Game implementations
│ ├── views.ts # Client-side view registry
│ └── number-guess/ # Example game
│ ├── config.ts # Game logic
│ └── GameView.tsx # Game UI
└── lib/ # Utilities
└── playerIdentity.ts # localStorage identity helper
Made with care. Have fun building games with your friends!