Auth-gated 2D & 3D multiplayer game demos on a shared realtime layer.
- Host / serve: Next.js (App Router) on Vercel
- Auth + Realtime: Supabase (OAuth sign-in, Realtime presence + broadcast)
- 2D rendering: Phaser
- 3D rendering: React Three Fiber + drei (Three.js)
Every game shares one abstraction — useRoom() — which joins a
Supabase Realtime room and exposes a presence roster, a stream of remote
positions, and a throttled sendMove(). To add a new game, drop in a
renderer and reuse the hook.
Browser ──(OAuth)──▶ Supabase Auth ──▶ session cookie
│
├─ Next.js on Vercel ── middleware gates /games/* (lib/supabase/middleware.ts)
│
└─ Supabase Realtime room ("room:<gameId>")
├─ presence → who's here (roster / lobby)
└─ broadcast → player positions, ~20 Hz, throttled in useRoom
Realtime here is a pub/sub relay, so the games are client-authoritative: each client simulates its own avatar and broadcasts position; peers interpolate. That's ideal for trusted demos. If a game later needs server authority (anti-cheat, conflict resolution), add a Supabase Edge Function for that game.
| Path | What |
|---|---|
lib/useRoom.ts |
Shared realtime room hook (presence + broadcast + sendMove) |
lib/identity.ts |
Player types, deterministic color, name helpers |
lib/supabase/* |
Browser / server / middleware Supabase clients (@supabase/ssr) |
middleware.ts |
Refreshes session, gates /games/* |
app/login |
OAuth sign-in (GitHub / Google) |
app/auth/callback |
Exchanges the OAuth code for a session |
app/games |
Gated hub + per-game routes |
components/games/Arena2D.tsx |
Phaser 2D reference game |
components/games/Space3D.tsx |
React Three Fiber 3D reference game |
From supabase.com, create a project and grab the Project URL and anon public key (Project Settings → API).
Authentication → Providers → enable GitHub and/or Google, and paste in the provider's client id/secret. Then under Authentication → URL Configuration:
- Site URL:
http://localhost:3000(and your Vercel URL in prod) - Redirect URLs: add
http://localhost:3000/auth/callbackandhttps://<your-app>.vercel.app/auth/callback
The login UI offers GitHub and Google. Enable at least one; edit
app/login/page.tsxto change the set.
cp .env.local.example .env.local
# fill in NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEYnpm install
npm run devOpen http://localhost:3000 → you'll be sent to /login. Sign in, pick a game,
and open it in a second tab (or another browser) to see multiplayer.
- Push this repo to GitHub and import it in Vercel.
- Add
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEYas Environment Variables. - Add your Vercel domain to Supabase Site URL + Redirect URLs (step 2).
No separate game server to host — Realtime is fully managed by Supabase, which is exactly why this stack fits Vercel's serverless model.
- Create
components/games/MyGame.tsx(a client component). - Call
useRoom("my-game", self)and render with any 2D/3D library. - Add
app/games/my-game/page.tsxmirroring the existing pages (fetch the user server-side, passself). - Add a card to the list in
app/games/page.tsx.
- Per-user secured data: this scaffold uses anon-key Realtime channels gated by the page's auth. If you add Postgres tables with row-level security, mint a Supabase-compatible session for the user (already true here, since auth is Supabase) and write RLS policies.
- Physics: add
@react-three/rapierto the 3D game for collisions/gravity. - Rooms / lobbies:
useRoomtakes any room id — make it dynamic (useRoom(match-${matchId}, self)) for private matches.
MIT — see LICENSE. Dice models, textures, and sounds ship via the MIT-licensed @drdreo/dice-box-threejs.