2026
Matcha
Matcha is a dating web application built as a 42 school project. It connects people by compatibility (orientation, shared interests, distance and fame rating), with real-time chat and notifications.
My role
Two-person 1337 team. I owned the backend: REST API design, PostgreSQL schema, Redis session caching, Socket.io real-time events, and JWT/OAuth 2.0 authentication.
Context
Users register with email verification, complete a profile, browse and search suggestions, and like each other. Chat opens only when a like is mutual. The team also built Google and 42 OAuth, an interactive map of nearby users, scheduling of real-life dates, a photo gallery with filters, and a dark and light theme.
Problem
People need to find compatible profiles by orientation, interests, distance and popularity. Chat has to open only when both people have liked each other, so nobody can message someone who has not shown interest. The 42 spec also required raw SQL, with no ORM.
Objectives
- Register, verify email, log in and set up a profile with photos, bio, orientation, interest tags and location.
- Suggest and search profiles by orientation, distance, shared tags, age and fame rating.
- Like, unlike, match, block and report, with chat unlocked by a mutual like.
- Deliver chat, notifications and online status in real time.
What I built
Authentication
JWTs travel in http-only cookies (SameSite Strict in production). Each token carries an ID, and on logout that ID goes into a Redis blocklist for the token's remaining lifetime, so revocation is immediate. Google and 42 OAuth link to an existing account by email, and reject a login when the account already uses a different provider.
Data model
Every query is parameterized raw SQL through the pg pool. Users have UUID ids, and other tables use serial ids. Likes, visits, blocks and reports use composite primary keys made of both user ids. Notifications are grouped with partial unique indexes and an upsert, so ten unread messages from one person show as one notification with a count.
Matching and search
Suggestions respect orientation in both directions, exclude blocked and already connected users, and rank by distance first, then shared tags, then fame rating. They can be sorted and filtered by age, location, fame and common tags. Distance is computed in PostgreSQL. Fame rating is a score from 0 to 100 built from likes, visits, blocks and profile completeness, and it is recalculated after every change that affects it.
Real time
One Socket.io connection per user carries chat messages, live notifications for likes, visits, matches and messages, and online status. Online state lives in Redis.
Architecture
A React 19 and TypeScript client (Vite, Tailwind CSS) talks to an Express API that is only a router, with no ORM, on PostgreSQL 16 with raw SQL. Redis handles caching, the JWT blocklist and online status. Socket.io carries chat and notifications. Nginx is the reverse proxy and terminates SSL, and Docker Compose runs the stack.
Key decisions and trade-offs
Use raw SQL through pg instead of an ORM.
Trade-off: The spec required it, and browse and search need dynamic filters, computed columns (distance, shared tags, age) and custom sorting that an ORM would push into raw SQL anyway. The cost is more SQL to write and review by hand. I rejected a query builder (Knex) because it would add a dependency without solving the dynamic queries.
Keep the JWT in an http-only cookie, with a Redis blocklist for logout.
Trade-off: A script cannot read an http-only cookie, so XSS cannot steal the token, and the blocklist makes logout take effect immediately instead of waiting for expiry. The cost is a Redis lookup on every authenticated request. I skipped CSRF tokens because SameSite Strict cookies already stop cross-site requests in production.
Compute distance in PostgreSQL, not in JavaScript.
Trade-off: Browse and search can filter and sort hundreds of candidates by distance in a single query. Doing it in JavaScript would need all candidates fetched first and sorted in memory. The cost is that the logic lives in the database, so a JavaScript copy exists for the few places outside SQL, such as a profile view.
Return 404, not 403, for a blocked profile.
Trade-off: A 403 would confirm that a block exists. A 404 looks the same as a user who does not exist, so neither person can tell who blocked whom. The cost is a less specific error for the client.
Track online status in Redis, not in the database column.
Trade-off: A database column races across multiple tabs and reconnects. Redis reflects the real number of active connections atomically. The column stays for persistence, such as showing last seen after Redis is cleared.
Outcome
The project covers the core spec and the bonus features: Google and 42 OAuth, an interactive map, scheduling of real-life dates, a photo gallery with crop, rotate and filters, and a dark and light theme.
Stack
- React
- TypeScript
- Vite
- Tailwind CSS
- Node.js
- Express
- PostgreSQL
- Redis
- Socket.io
- Nginx
- Docker