Real-time multiplayer trivia game (Kahoot-style) built with Next.js 16, React 19, and Tailwind CSS v4.
For local development, you need a Supabase project. Either create one at database.new and use its URL/keys, or run supabase start for local Supabase and use http://127.0.0.1:54321 with the local keys.
pnpm install
cp .env.example .env.local
# Edit .env.local with your Supabase URL, anon key, and service role key
pnpm devOpen http://localhost:3000 in your browser.
- Host creates a game at
/host/create— sets a game password, teams, and uploads a questions JSON file - Host receives a 6-character game code and is redirected to the dashboard at
/host/[code] - Players join at
/play/[code]— enter the game password, a display name, and select a team - Host controls the game flow: Start Game → Next Round → End Round → Show Results → End Game
- Players answer questions in real-time, scored on correctness and speed
- Host and players use anonymous Supabase sessions
- All mutations (create game, join, answer, host controls) go through Next.js API routes
- API routes use
service_roleto write to Postgres and publish Broadcast events - Clients subscribe to Supabase Realtime Broadcast channels for real-time game state (round start, answer counts, leaderboard, etc.)
flowchart TB
subgraph browser [Browser]
ClientJS["supabase-js (anon + anonymous auth)"]
ReactUI["React UI"]
end
subgraph nextjs [Next.js API Routes]
AdminJS["supabase-js (service_role)"]
GameLogic["Game logic + Broadcast publish"]
end
subgraph supabase [Supabase]
PG["Postgres (games, teams, players, questions, answers)"]
RT["Realtime Broadcast"]
Auth["Auth (anonymous sessions)"]
end
ReactUI --> ClientJS
ClientJS --> Auth
ClientJS --> RT
ReactUI --> GameLogic
GameLogic --> AdminJS
AdminJS --> PG
AdminJS --> RT
Upload a JSON file when creating a game. The file should follow this schema:
{
"title": "My Trivia Game",
"questions": [
{
"text": "What is the capital of France?",
"options": [
{ "id": "a", "text": "London" },
{ "id": "b", "text": "Paris" },
{ "id": "c", "text": "Berlin" },
{ "id": "d", "text": "Madrid" }
],
"correctOptionId": "b",
"timeLimit": 20
}
]
}Each question requires:
text— the question textoptions— exactly 4 options, each withidandtextcorrectOptionId— theidof the correct optiontimeLimit— seconds for the round (5–120)
A sample file is provided at data/sample-questions.json.
- Correct answer:
Score = 100 × (1 - time_taken / time_limit) - Incorrect/no answer: 0 points
- Team score: sum of all team members' individual scores
- Next.js 16 with App Router and React Compiler
- React 19 with Supabase Realtime Broadcast for real-time updates
- Tailwind CSS v4 with dark theme
- Supabase — Postgres (games, teams, players, questions, answers), Auth (anonymous sessions), Realtime Broadcast (game events)
| Variable | Description |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase anon (public) key |
SUPABASE_SERVICE_ROLE_KEY |
Supabase service role key (server-only, never expose to client) |
Create .env.local with these values from the Supabase dashboard (Settings > API).
Prerequisites: Node 24 (or compatible), pnpm, Supabase CLI, Cloudflare account (for tunnel). Optional: mise for tooling (this project includes cloudflared and supabase in mise.toml).
Steps:
-
Clone the repo
git clone <repo-url> cd trivia pnpm install
-
Create a Supabase project
- Go to database.new or Supabase Dashboard
- Create a new project, note the URL and keys from Settings > API
-
Run migrations
supabase link --project-ref <your-project-ref> supabase db push
Or manually: run the SQL in
supabase/migrations/00001_initial_schema.sqlin the Supabase SQL Editor -
Enable anonymous auth (if not already)
- Supabase Dashboard > Authentication > Providers > Anonymous Sign-Ins: enable
-
Configure environment
- Copy
.env.exampleto.env.localand fill in Supabase URL, anon key, service role key
- Copy
-
Build and run locally
pnpm build pnpm start
-
Expose via Cloudflare Tunnel
- Install
cloudflared(e.g.brew install cloudflaredor via mise) - Quick tunnel (temporary URL, for testing):
cloudflared tunnel --url http://localhost:3000
- Named tunnel (persistent hostname):
cloudflared tunnel logincloudflared tunnel create trivia- Create
~/.cloudflared/config.ymlwithurl: http://localhost:3000and tunnel credentials cloudflared tunnel route dns trivia <your-subdomain>.yourdomain.comcloudflared tunnel run trivia
- Install
-
Supabase Auth redirect URLs (if using a custom domain)
- Supabase Dashboard > Authentication > URL Configuration
- Add your tunnel URL (e.g.
https://trivia.yourdomain.com) to Site URL and Redirect URLs
Important: This app is designed for temporary, single-game-session use when self-hosted via Cloudflare Tunnel. Long-running sessions and production-grade security (rate limiting, DDoS hardening, etc.) are not guaranteed. Use for casual games with trusted participants, not as a persistent public service.