How We Built GenzDating: Tech Deep Dive
Every dating app starts the same way: a whiteboard, big ambitions, and a "how hard can it be?" attitude. Six months later, here's the unfiltered story of how we actually built GenzDating — the architecture, the trade-offs, and the lessons learned.
Architecture Overview
GenzDating follows a modular monolith pattern — a single NestJS application with clearly separated modules. This gives us the development speed of a monolith with the organizational clarity of microservices. When (and only when) we need to scale, individual modules can be extracted into their own services.
+- Mobile App (Expo/RN) ---------------------+— Auth | Swipe | Match | Chat | Profile ... —— React Query + Zustand + Socket.IO Client —+----------------------------------------------+ — REST + WebSocket+- NestJS API (Monolith) --------------------+— Auth | Profiles | Swipe | Match | Chat —— Common: Guards, Pipes, Interceptors —+---------------------------------------------+ ? ? ? PostgreSQL Redis Cloudinary + PostGIS (Cache, (Media) Queue, Pub/Sub)
Database: PostgreSQL + PostGIS
Location-based discovery is central to any dating app. We chose PostgreSQL with PostGIS over MongoDB or a dedicated geo-service because:
- It integrates natively with our Drizzle ORM setup
ST_DWithinon aGEOGRAPHY(Point, 4326)column is fast and accurate- No additional infrastructure to manage
Our discovery query looks roughly like this:
SELECT p.*, ST_Distance( p.location::geography, ST_SetSRID(ST_MakePoint($lon, $lat), 4326)::geography) AS distanceFROM profiles pWHERE ST_DWithin( p.location::geography, ST_SetSRID(ST_MakePoint($lon, $lat), 4326)::geography, $maxDistance * 1000)AND p.user_id NOT IN (SELECT blocked_id FROM blocks WHERE blocker_id = $userId)AND p.user_id NOT IN (SELECT swiped_id FROM likes WHERE swiper_id = $userId)AND p.is_active = trueORDER BY distance ASCLIMIT 20
The GiST index on the geography column keeps this query under 5ms even with millions of rows.
Real-Time: Socket.IO + Redis
Chat and matching require instant delivery. We use Socket.IO with the Redis adapter for horizontal scaling. Here's the flow:
User A sends message ? Socket.IO emits 'chat:message' to room ? Redis adapter broadcasts to all instances ? User B receives in real-time ? BullMQ queue persists message to Postgres ? If User B offline ? FCM push notification
The BullMQ queue is critical — it decouples the real-time delivery from the database write, so users never experience lag even under heavy load.
Auth: Supabase (GoTrue)
We don't roll our own auth. Supabase Auth handles email/password, Google OAuth, Apple OAuth, OTP verification, password resets, and JWT issuance. Our NestJS backend validates JWTs via Supabase's JWKS endpoint in a custom AuthGuard. This saved us weeks of auth boilerplate.
Key Decisions & Trade-offs
- Drizzle ORM over Prisma: Drizzle is lighter, has no code generation step, and gives us raw SQL access when needed. We sacrificed some ergonomics for performance and control.
- Cursor pagination over offset: For feeds (discover, matches, messages), cursor pagination is faster and consistent even when data changes between pages.
- Modular monolith over microservices: For our current scale, microservices would add complexity without benefit. We'll extract when profiling shows a bottleneck.
- PostGIS over a dedicated geo-service: One less dependency. PostGIS is battle-tested and fast enough for millions of users.
What We'd Do Differently
Hindsight is 20/20. Here are a few things we'd change if starting over:
- Add Redis earlier. We initially tried to do rate limiting in Postgres. Bad idea. Redis was a 10-line fix that cut API latency by 40%.
- More aggressive indexing. The
likestable needs composite indexes on(swiper_id, swiped_id)— we learned this the hard way during load testing. - TypeScript end-to-end from day one. We had some early JavaScript that caused runtime errors. Rewriting in strict TypeScript caught dozens of bugs immediately.
Bottom line: Building a dating app is harder than it looks — but choosing the right stack makes all the difference. PostGIS, Socket.IO, Redis, and Supabase are a winning combination for real-time, location-based social apps.
— The GenzDating Engineering Team
Back to Blog