A premium, mobile-first multiplayer jigsaw puzzle racing game. Upload a photo, invite friends with a link, and race to solve the same puzzle first.
Built as a single Next.js 15 project — no separate backend, no WebSocket server. Deploys directly to Vercel.
- Framework: Next.js 15 (App Router) + TypeScript
- Styling: Tailwind CSS + Framer Motion + Lucide React
- Backend: Next.js API Routes (serverless)
- Database: PostgreSQL via Prisma ORM (designed for Neon on Vercel)
- Image processing: Sharp
- Storage: Supabase Storage by default, with a swappable abstraction (Cloudflare R2 or local disk also supported)
- Drag & drop: dnd-kit (mobile touch support built in)
- Sync: Database polling (no WebSocket server required)
app/
page.tsx Home page
create/page.tsx Create Room page
room/[code]/page.tsx Lobby
room/[code]/join/page.tsx Join page (nickname only)
room/[code]/play/page.tsx Live gameplay
room/[code]/results/page.tsx Leaderboard
api/rooms/create/route.ts Create room + generate puzzle
api/rooms/[code]/route.ts Get room info
api/rooms/[code]/join/route.ts Join room
api/rooms/[code]/players/route.ts Poll players (lobby)
api/rooms/[code]/start/route.ts Host starts game
api/rooms/[code]/pieces/route.ts Get puzzle pieces
api/rooms/[code]/move/route.ts Submit a move (server-validated)
api/rooms/[code]/state/route.ts Poll live game state
api/rooms/[code]/leaderboard/route.ts Final/live leaderboard
components/
ui/ Button, Card, Skeleton, AnimatedBackground
game/ PuzzleBoard, PuzzlePiece, PieceTray, RoomCard, PlayerList,
Leaderboard, Timer, UploadBox, DifficultySelector, WinnerModal
lib/
prisma.ts Prisma client singleton
storage/ Storage abstraction (Supabase / R2 / local)
game/
difficulty.ts Grid size configs (3x3, 4x4, 6x6, 8x8)
room-code.ts Short invite code generator
puzzle-generator.ts Sharp-based image splitting
validation.ts Server-authoritative move/progress validation
hooks/usePlayerSession.ts LocalStorage-backed player identity per room
prisma/schema.prisma Room, Player, PuzzlePiece models
- Host creates a room — uploads an image, picks a difficulty, enters a nickname.
The server validates the image with Sharp, resizes it to a square canvas, splits
it into pieces, uploads each piece + the full preview to storage, and creates the
Room,Player(host), andPuzzlePiecerows in one request. - Players join via
/room/[code]/join— just a nickname, no account needed. - Lobby (
/room/[code]) polls/api/rooms/[code]/playersevery 2s and shows the live player list, puzzle preview, and room code/invite link. Only the host sees the "Start Game" button. - Host starts the game — the server sets
Room.startedAta few seconds in the future so every client's countdown (3, 2, 1, GO) is synced to the same moment. - Gameplay — pieces are shuffled into a tray; players drag pieces onto the grid
using dnd-kit (full touch support). Every move posts the current board state to
/api/rooms/[code]/move. The server — not the client — computes progress %, increments the move counter, and determines completion. Finish time is alwaysnow - Room.startedAt, computed server-side, never trusted from the client. - Results (
/room/[code]/results) polls the leaderboard every 3s, ranked by fastest completion time, then fewest moves — with confetti for the current player's own completion viaWinnerModal.
Per the spec, there's no persistent WebSocket server — this keeps the whole app deployable as stateless serverless functions on Vercel. Lobby and results poll every 2–3 seconds, which is more than responsive enough for a casual multiplayer puzzle game.
npm installcp .env.example .envFill in:
DATABASE_URL/DIRECT_URL— your Neon Postgres connection strings. If you're using Vercel + Neon: go to your Vercel project → Storage → Create Database → Neon Postgres. Vercel auto-populates both variables for you (locally, copy them from the Neon dashboard's "Connect" panel — use the pooled connection string forDATABASE_URLand the direct one forDIRECT_URL).- Supabase Storage (default provider):
- Create a free project at supabase.com.
- Go to Storage → create a bucket named
puzzle-imagesand mark it Public. - Go to Project Settings → API and copy the Project URL and
service_role key into
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY.
npx prisma db push(Or npm run db:migrate if you prefer tracked migrations.)
npm run devVisit http://localhost:3000.
- Push this repo to GitHub.
- Import it into Vercel.
- Add a Neon Postgres database from the Vercel Storage tab (auto-fills
DATABASE_URL/DIRECT_URL). - Add the Supabase env vars (
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,SUPABASE_BUCKET) in Project Settings → Environment Variables. - Set
NEXT_PUBLIC_APP_URLto your production URL (e.g.https://your-app.vercel.app) so invite links are correct. - Deploy. Vercel runs
prisma generate && next buildautomatically via thebuildscript andpostinstallhook.
The storage layer is fully abstracted behind StorageProvider in lib/storage/.
To switch from Supabase to Cloudflare R2, just change one environment variable:
STORAGE_PROVIDER="r2"
R2_ACCOUNT_ID="..."
R2_ACCESS_KEY_ID="..."
R2_SECRET_ACCESS_KEY="..."
R2_BUCKET="..."
R2_PUBLIC_URL="https://your-bucket.r2.dev"No application code changes required — getStorageProvider() picks the right
implementation automatically.
- All game-critical values (moves, progress, finish time, completion) are computed
server-side in
/api/rooms/[code]/move. The client only ever sends the current board layout; it cannot claim an arbitrary time or move count. - Room codes are 6 characters from an unambiguous alphabet (no
0/O,1/I) and checked for collisions on creation. - Image uploads are validated (type, size, real decodable image, minimum dimensions) before any processing happens.
| Level | Grid | Pieces |
|---|---|---|
| Easy | 3×3 | 9 |
| Medium | 4×4 | 16 |
| Hard | 6×6 | 36 |
| Expert | 8×8 | 64 |
Built for demonstration purposes. Customize freely.