A real-time, browser-based social deduction party game. Everyone in the room gets the same word — except the imposter, who gets something close enough to bluff with. Describe, discuss, and vote before the imposter talks their way out of it.
Built with a plain Node.js/Express backend and a single-page vanilla JS
frontend, wired together over WebSockets. No build step, no framework,
no database — just clone it, npm install, and play on your local network.
- How it plays
- Play styles
- Quick start
- Deployment
- Project structure
- Configuration
- Adding your own categories
- Architecture notes
- Available scripts
- Roadmap
- Support
- License
- Create a room. One player creates a room and shares the 4-character room code with everyone else.
- Join. Other players join using that code (3–10 players per room).
- Roles are assigned. Every round pulls a random category from the content pack. Everyone gets the majority word or question — one random player secretly gets the imposter version instead.
- Describe or answer. Players take turns, in a shuffled order, describing their word (or answering their question) out loud without saying it outright.
- Vote. Once everyone's had a turn, the room votes on who they think the imposter is. Ties are broken at random.
- Reveal. The eliminated player is revealed, along with whether they were actually the imposter and what the real words/questions were.
- Play again. The host can send everyone back to the lobby for another round — the game remembers recently used sets so you won't see the same pairing again too soon.
Player names must be unique within a room — if a name is already taken (case-insensitive), the join is rejected so nobody gets confused about who's who.
There are two content modes, mixed into the same pool:
| Mode | What happens |
|---|---|
| Word | Everyone describes a word (e.g. Pencil) — the imposter secretly has a different, related word (Pen) and has to fake it. |
| Question | Everyone answers a question about a topic (e.g. "How often do you take it for walks?" about a Dog) — the imposter answers a different but similarly-shaped question about a different topic (Cat). |
The host chooses how players take their turns, via a Game Mode toggle in the lobby:
| Style | What happens |
|---|---|
| Typed (default) | Each player types their description/answer on their turn; everyone sees the answers build up in a shared feed before voting. |
| Live Play 🎙️ | Players describe out loud, in person or on a call — no typing. The app just tracks the speaking order and shows whose turn it is; the current speaker (or the host) taps to pass the turn. |
Live Play is ideal when everyone's in the same room and only needs their phones to see their secret word and to vote.
Requirements: Node.js 18 or later.
# 1. Clone the repo
git clone https://github.com/dev-rjav/Imposter
cd Imposter
# 2. Install dependencies
npm install
# 3. (Optional) copy the env template if you want to override any defaults
cp .env.example .env
# 4. Start the server
npm startThe server logs the URL it's listening on:
🎮 Imposter Game → http://localhost:3000
📦 111 sets (81 word, 30 question) across 34 categories
🔄 Tracks last 20 games per room to avoid repeats
Open that URL in a browser. To play with others on the same WiFi, share
http://<your-local-ip>:3000 instead of localhost — every player just
needs to be able to reach your machine on the network.
The app is a single Node service: Express serves the frontend and runs the
WebSocket game server on the same origin. The frontend connects to
window.BACKEND_URL if set, otherwise it falls back to same-origin — so the
default deploy needs no configuration.
Any platform that runs a persistent Node process works — Railway, Render, Fly.io, a VPS, etc.
- Push this repo to GitHub and create a new project from it on your host.
- The platform installs dependencies and runs
npm startautomatically. No env vars are required —config.jsreads thePORTthe platform provides. - Generate/open the public domain. Because the WebSocket is same-origin,
leave
window.BACKEND_URLempty. Done.
⚠️ The backend needs persistent WebSocket connections, so a static / serverless-only host (e.g. plain Vercel or GitHub Pages) cannot run it.
If you want the static frontend on one host and the WebSocket backend on another (e.g. frontend on Vercel, backend on Railway):
-
Deploy the backend as above and copy its URL.
-
In
public/index.html, set the backend URL (must bewss://over HTTPS):window.BACKEND_URL = "wss://your-backend.example.com";
-
Deploy the
public/directory as a static site (Framework: Other, Root Directory:public, no build command).
imposter-game/
├── data/ # All game content — the only place word/question
│ │ # sets live. server.js never hardcodes content.
│ ├── wordSets.js # "describe a word" round content, by category
│ ├── questionSets.js # "answer a question" round content, by category
│ └── index.js # merges both into ALL_SETS + derives category stats
├── public/ # Static frontend served as-is by Express
│ └── index.html # single-page UI: lobby, game, voting, results
├── .github/ # Issue templates, PR template, CI workflow
├── config.js # Centralised, environment-driven server settings
├── server.js # Express + WebSocket game server (all game logic)
├── .env.example # Documents every configurable environment variable
├── package.json
└── README.md
The server reads its settings from environment variables, all optional —
sensible defaults are defined in config.js. Copy
.env.example to .env to override any of them:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the HTTP + WebSocket server listens on |
MAX_PLAYERS_PER_ROOM |
10 |
Max players allowed in a single room |
MIN_PLAYERS_TO_START |
3 |
Minimum players the host needs before starting |
SET_HISTORY_SIZE |
20 |
How many previous sets a room remembers, to avoid repeats |
ROLE_REVEAL_DELAY_MS |
5000 |
Delay before the first describer's turn, so players can read their word |
Game content is fully decoupled from game logic, so you can extend it without
touching server.js at all.
To add a word-guessing pair, append an entry to data/wordSets.js:
{ type: "word", category: "Your Category", majority: "Real Word", imposter: "Close-but-different Word" },To add a question pair, append an entry to data/questionSets.js:
{
type: "question",
category: "Your Category",
majority: "Topic A 🎯",
imposter: "Topic B 🎲",
majorityQ: "A question that makes sense for Topic A.",
imposterQ: "A similarly-shaped question that makes sense for Topic B.",
},Both files export a plain array — nothing else needs to change. data/index.js
automatically merges them into ALL_SETS and recomputes category counts on
the next server start.
- No database. All game state lives in memory (
Maps keyed by room code / WebSocket connection) insideserver.js. Restarting the server clears every room — this is intentional for a lightweight party game, not an oversight. - One WebSocket connection per player, tracked alongside their room code and player ID. Every state-changing action broadcasts a fresh room state to all connected players in that room, so clients never need to diff state themselves.
- Repeat avoidance is per-room, not global —
room.recentHistorystores the indices of the lastSET_HISTORY_SIZEsets used in that specific room, so two rooms playing simultaneously don't affect each other. - Host migration. If the host disconnects, the next remaining player in the room is silently promoted to host.
| Command | What it does |
|---|---|
npm start |
Starts the server |
npm run dev |
Starts the server with Node's --watch flag for auto-restart on file changes |
npm run lint |
Runs ESLint over the project |
npm test |
Placeholder — no automated tests yet, see Roadmap |
- Automated tests around room lifecycle and vote resolution
- Optional category-pack selection from the lobby before starting
- Reconnect support for players who briefly lose connection mid-round
If you enjoy the game and want to support its development:
Stars on the repo are appreciated too — they help other people find it.
MIT — do whatever you'd like with it.