Transcendence

A full-stack multiplayer Pong game with real-time WebSocket communication and user social features

A 42 School curriculum project that combines game development, real-time communication, and social networking. The architecture seamlessly integrates a 3D game engine with a structured backend API and responsive frontend, demonstrating both systems-level design and user experience considerations.

TECH STACK
TYPESCRIPT
FASTIFY
PRISMA
SQLITE
BABYLONJS
SOCKET.IO
ZOD
NODEMAILER
DOCKER
GRAFANA
JWT
NGINX
VITE

Why I Built This

The transcendence project addresses a fundamental challenge in fullstack education: how to ship a production-grade system that combines real-time interactivity, data persistence, and user experience in a single codebase. Most tutorials silently gloss over session state, connection reliability, or multi-step user flows.

The core insight driving architecture decisions was separation of concerns without artificial boundaries. Rather than a monolithic 'call the backend, render the response' pattern, the system splits game logic (client-side, fast-loop), user management (server-side, transactional), and real-time state (bidirectional over WebSocket). This separation forces clarity: a move in Pong is client-authoritative with server-reconciliation; a friend request is server-authoritative with eventual client sync.

Every dependency was chosen to minimize complexity-per-feature. Fastify over Express for simpler plugin composition. Prisma over raw SQL for migration safety. Zod for compile-time inference of validation schemas. Socket.io for abstraction over raw WebSockets. These choices compound: schema validation automatically generates Swagger docs; Prisma migrations track database evolution; Zod + TypeScript prevent entire categories of null/undefined bugs.

Transcendence

Architecture Overview

The system layers into three tiers that communicate through explicit boundaries: REST endpoints for stateful operations (auth, user management, match records), WebSocket events for real-time state (game ticks, chat), and a persistent SQLite database as the source of truth.

Game Engine & Rendering

Client-side BabylonJS scene with interactive physics simulation (Pong ball collision, paddle movement). Inputs queue as local events; server state reconciles on each Socket.io frame. The game loop decouples rendering (RAF-driven) from network I/O (event-driven), preventing dropped frames under poor connectivity.

API & Real-time Sync

Fastify HTTP server + Socket.io namespace for matchmaking and live gameplay. Zod schemas validate all inputs; server actions mutate state atomically via Prisma transactions. Session state persists as JWT cookies; WebSocket authentication pipes user context into event handlers.

Persistence & Infrastructure

SQLite database with Prisma ORM. Schema includes Users (email, hashed password, profile), Friends (directed edges, pending state), Matches (two-player results with scores). Email verification uses nodemailer + Gmail SMTP. Docker Compose orchestration for local development.

Game state is client-authoritative for rendering but server-reconciled for fairness. Match creation is transactional via Prisma, preventing race conditions if both players create a match simultaneously.

EXPLORER
backend
src
frontend
src
docker-compose.yml # Local dev environment

Core Features

User Authentication & Email Verification

End-to-end authentication flow: client POSTs email + password → backend hashes + stores user → nodemailer sends verification link → user clicks email link → verify-email endpoint updates emailVerified flag.

• Account CreationPOST /api/users with { email, name, password }. Backend validates email format (Zod), generates bcrypt hash, creates user record, queues verification email.
• Email VerificationGET /api/users/verify-email?token={uuid}. Token is a UUID stored in user record at signup. Endpoint updates emailVerified=true, handles expiry via timestamp.
• Login FlowPOST /api/users/login with { email, password }. Server retrieves user, compares bcrypt hash, returns JWT if match. JWT includes userId claim for session context.

Friend Request Workflow

Models a directed friend graph with pending state. Enables users to propose connections without mutual agreement.

• Add FriendPOST /api/users/friends with { userId, friendId }. Creates record with status: 'pending'.
• AcceptPATCH /api/users/friends/accept with { userId, friendId }. Sets status: 'accepted'. Creates reverse edge if not present.
• Refuse/DeleteDELETE /api/users/friends or PATCH .../refuse. Removes record.
• List FriendsGET /api/users/friends/list/{userId}. Returns all status: 'accepted' edges where userId is either sender or receiver.

Multiplayer Match Coordination

Tracks Pong game sessions: creation (two players invited), gameplay (real-time via WebSocket), result recording.

• Create MatchPOST /api/matches with { player1Id, player2Id }. Server creates match record with status: 'pending'. Returns match ID for WebSocket join.
• Join GameClient emits match:join { matchId } over Socket.io. Server adds socket to namespace, broadcasts players:ready when both connected.
• Game LoopBoth clients emit ball:position { x, y, velocityX, velocityY } every frame; server broadcasts to opponent + reconciles position if drift > threshold.
• End MatchPATCH /api/matches/{id}/result with { scorePlayer1, scorePlayer2, winnerId }. Server atomically updates match record + increments player stats.

Backend Systems

Auth

Fastify plugin wraps @fastify/jwt. On server start, reads JWT_SECRET from env. Routes decorated with @fastify/jwt check Authorization header; if valid, inject request.user object. Logout clears HTTP-only cookie on client.

Email Service

Nodemailer transporter configured for Gmail SMTP. Env vars: EMAIL_USER (full email), EMAIL_PASSWORD (app-specific password from Google Account Security). Service method sendVerificationEmail(email, token) constructs HTML template + sends. Errors logged; non-fatal (user can re-request email).

Match Recording

Server action triggered by client match:end event. Validates both players submitted results (prevent one-sided claims); if conflict, reject match. On agreement, POST /api/matches/{id}/result atomically updates match record + increments winner's stats in a single Prisma transaction.

Key Decisions

BabylonJS over Canvas

BabylonJS abstracts WebGL complexity: physics engine (collider detection), camera management, lighting. Canvas would require building all this. Trade: larger bundle (8MB uncompressed). Justification: rapid iteration on game feel + 3D capabilities for future features (power-ups, arena themes). Client sees instant visual feedback; network lag doesn't feel like input latency because local simulation runs decoupled.

Fastify over Express

Fastify ships plugin hooks (onRequest, onResponse) that compose cleanly. Express middleware is sequential but less typed. Fastify's stricter schema validation (via fastify-zod) enables Swagger generation automatically. Trade: smaller ecosystem. Justification: for a 42 project, shipping documentation + type safety pays off more than dependency count.

JWT over Session Cookies

JWT avoids server-side session store (simpler deploy, horizontal scaling possible). Socket.io middleware decodes JWT from handshake headers. Trade: token revocation requires blacklist. Justification: for a student project, stateless auth is cleaner; logout soft-deletes via client cookie clear.

What I Learned

Real-time state is deceptively complex

The game loop feels instant locally but reconciling with opponent state over network reveals every assumption. A naive implementation broadcast every position change; optimized version batches updates at 30 Hz + uses delta-compression (only send changed fields). Result: 60% fewer WebSocket messages.

Database transactions are non-negotiable

Match result recording: if both players send { score1: 10, score2: 5 } simultaneously, race condition could double-record. Wrapping the query in prisma.$transaction() with Pessimistic Locking forced all mutations into serial order. Cost: brief lock contention. Benefit: correctness.

Email delivery is unreliable by design

Gmail sometimes flags verification emails as spam. Solution: add 'Resend Email' button on frontend; backend resets token on each request. No exponential backoff; idempotent.

Type inference compounds

Zod schema auto-generates TypeScript types; passing those to Prisma results in end-to-end type safety. A rename to emailVerified cascades type errors from API request → business logic → database layer. Caught three refactoring bugs before hitting production.

Background

SUCCESS IS NO ACCIDENT, IT IS HARD WORK.

LET'S MAKE IT HAPPEN!

MAKE IT WORK. MAKE IT RIGHT. MAKE IT FAST. — Kent Beck

Get In Touch

I'm always open to discussing new opportunities, creative projects, or just having a chat about technology.