2025
ft_transcendence
A real-time social gaming platform built around Pong: local, online and tournament matches, chat, friends and statistics, on a custom JSX single-page app and seven microservices.
My role
Team project. I built the full frontend: the TypeScript single-page app, its custom JSX runtime, the router, and every page and component.
Context
ft_transcendence is the final project of the 42 curriculum: a social gaming platform that combines a Pong game with accounts, chat and friends.
Problem
Pong is simple, but a platform around it is not. Players need secure accounts, real-time matches and chat, friends and blocking, and a history of their games, all in one app.
Objectives
- Play Pong locally, against a remote player, or in a tournament.
- Secure accounts: JWT sessions, two-factor authentication (TOTP or email), and Google and 42 OAuth.
- Real-time chat and direct messages, friend requests, blocking and privacy controls.
- Match history and statistics, with charts.
How the JSX engine works
Where the design comes from
The runtime is based on a published design for custom JSX with Vite and TypeScript, which I adapted for this project. The router, the pages, the components and the event handlers built on top of it are mine.
JSX becomes plain function calls
TypeScript is set to jsx react-jsx with jsxImportSource "@", and Vite's esbuild is told to use jsx.component as the factory and to inject import { jsx } from '@/jsx-runtime' into every file. So <div className="card">Hi</div> compiles to jsx.component("div", { className: "card" }, "Hi"). React is never involved.
One 33-line function builds real DOM
jsx.component(component, props, ...children) puts the flattened children on props.children. A function component is called with those props. A tag name creates an element with document.createElement, every prop except children becomes an attribute (className is mapped to class), and each child is appended as a DOM node or as a text node. It returns a real element immediately, with no virtual DOM.
Pages are functions that return elements
Every page, such as Signin, Home or Game, is a function that returns an HTMLElement, so JSX composes with ordinary functions. The router maps route names to those functions. Links marked data-link are intercepted, history.pushState updates the URL, and the router renders the new page.
Types, and what the runtime leaves out
A global JSX.IntrinsicElements declaration accepts any tag name with any props, so TypeScript allows every element but does not check its props. The runtime has no state, no re-rendering or diffing, and no event props. Event listeners are attached after render in separate handler modules, which find the form by id and call addEventListener.
Architecture
A Vite and TypeScript single-page app sits in front of seven domain-driven Node.js microservices on Fastify: auth (2FA and OAuth), profile, relationships (friends and blocking), chat, game, dashboard and notifications. The services use SQLite databases and Redis caching, and talk to each other over RabbitMQ on the Docker network. Nginx is the reverse proxy and terminates SSL, and Docker Compose runs everything. The client renders with a custom JSX engine instead of a UI framework.
Key decisions and trade-offs
Split the backend into seven domain-driven microservices.
Trade-off: Each domain, such as auth, chat or game, can change on its own. The cost is a message queue and several databases that have to stay consistent.
Use a small custom JSX runtime that builds DOM directly, instead of adopting a UI framework.
Trade-off: The runtime is 33 lines with no dependencies, and JSX still compiles to real DOM nodes with TypeScript checking tag names. The cost is that there is no state, no re-rendering or diffing, and no event props, so handlers are attached by id after render and a page changes by rendering again.
Outcome
Pong with local, remote and tournament modes, chat, friends and blocking, match statistics, and login with 2FA and Google or 42 OAuth. Matchmaking is built for 500+ simultaneous players.
Stack
- TypeScript
- Vite
- Tailwind CSS
- Custom JSX
- Chart.js
- Node.js
- Fastify
- SQLite
- Redis
- RabbitMQ
- WebSocket
- Nginx
- Docker