What it does
Conversa is a mobile app for meeting people through conversation. When you sign up you pick a track: dating, or language exchange. The two tracks never mix, so someone practising Spanish is never shown dating profiles, and the reverse is also true. On the language track you list your native and target languages and what you want from the exchange: conversation, structured practice, or meeting for coffee. Your discovery deck is built from those choices. A Turkish speaker learning English sees English speakers learning Turkish first, then people learning the same language.
Every like is a conversation request with a countdown. You decide how many new requests reach you each day. Inside a chat you can send voice messages, start with an icebreaker, rewrite a message in a different tone, get corrections on what you wrote, translate a line, and book a timed practice session that shows how evenly the two languages were used. Language levels on profiles come from a short AI-graded exam, and a selfie check confirms that the person matches their photos.
Key features
- Two separate tracks (Date / Language). The feed, likes, favorites and requests all stay within your track, and switching has a 30-day cooldown.
- Language-aware matching. Partners are ranked in three tiers: full swap, shared target language, or one-way practice. Filters cover age, distance, native language and target language.
- Verified language levels. A rotating-item exam is graded by an LLM to a CEFR level (A1–C2). The resulting proof is valid for 12 months, and each language can be retaken every 7 days.
- Requests with recipient control. Each user sets a daily intake limit. Requests over that limit queue quietly, and unanswered ones expire on their own.
- Real-time chat. Socket.IO messaging with typing indicators, conversation updates, voice messages, icebreakers and unread counts.
- Learning tools in chat. AI polish with tone control, partner and AI corrections saved to a personal mistake book, translation, and scheduled practice sessions with a language-balance readout.
- Trust and safety. Automated face verification, blocking and reporting, and report-triggered content screening that can lead to a warning or a ban.
- Monetization. Premium subscriptions and consumable packs through RevenueCat, AdMob rewarded and banner ads, profile boosts and referrals.
How it’s built
The system has two repositories: an Expo / React Native client and a TypeScript Express 5 API. The API is organized as feature modules behind /api/v1 (auth, discovery, likes, requests, chat, exam, coach, sessions, billing, boost, rewards, referral, safety, storage, webhooks, admin). Shared domain logic sits in src/lib. PostgreSQL is the source of truth through Prisma 7 with the pg driver adapter. The schema has about 20 models, including ConversationRequest, LanguageProof, Correction, LanguageSession and BillingEvent, and has gone through 29 migrations. Socket.IO shares the REST session model: the handshake token is hashed and looked up the same way as a REST request. The server keeps a map from each user to their sockets so it can push new_message, new_match, new_chat_request and typing events, and it disconnects every socket when a session is revoked.
Most of the engineering is in the anti-spam and fairness rules. Opening a conversation is limited in two independent ways in conversationSlots.ts. A concurrency cap limits how many people you can leave waiting for an answer, and a daily throughput cap stops users from repeatedly sending and withdrawing requests to get around the first cap. Recipients set their own intake in intake.ts, and requests over it are stored as QUEUED instead of being declined. An hourly sweeper expires stale requests per track, and a nightly job reconciles the denormalized openUnansweredCount counter against the real rows so a drifted counter cannot silently lock a user out. Language detection with tinyld drives a behavior score: target-language ratio, reciprocity, reply rate and how varied a user’s opening messages are. New accounts get a 14-day grace period and a minimum sample size before the score has any effect.
AI calls all go through one provider layer, modules/ai/provider.ts. It can route to OpenAI, OpenRouter or an OpenAI-compatible gateway, with a separate model setting for each task: exam, coach, translate and polish. Every structured output is parsed defensively. A malformed exam verdict scores 0, and a malformed coach verdict writes nothing, because a false correction does more harm than no correction. Face verification runs on the server at no per-call cost: @vladmandic/face-api on TensorFlow.js with the WASM backend compares 128-dimensional face descriptors from a pose selfie against the profile photos.
On mobile, Expo Router 6 provides file-based navigation ((auth), (tabs), conversation/[id], premium). The client uses React 19 and React Native 0.81 with Reanimated 4 for the swipe deck and i18next for English and Turkish. Platform-specific .web.ts shims for ads, purchases and secure storage let the same codebase run under react-native-web. A GitHub Actions workflow bumps versions, generates release notes from commits, builds the Android bundle with Gradle and sends the iOS build through EAS to TestFlight.
Tech stack
- Mobile
- React Native 0.81
- Expo SDK 54
- Expo Router 6
- React 19
- TypeScript
- Reanimated 4
- Gesture Handler
- react-native-keyboard-controller
- expo-audio
- expo-image-picker
- expo-location
- expo-secure-store
- expo-blur
- expo-linear-gradient
- react-native-svg
- i18next / react-i18next
- Axios
- Zod
- react-native-web
- Auth
- One-time email codes (HMAC-hashed)
- Google Sign-In (`@react-native-google-signin/google-signin`)
- Sign in with Apple (`expo-apple-authentication`)
- hashed session tokens
- Backend
- Node.js (ESM)
- TypeScript
- Express 5
- Zod
- Helmet
- CORS
- express-rate-limit
- Multer
- Morgan
- Nodemailer
- jsonwebtoken
- Realtime
- Socket.IO 4 (server and `socket.io-client`)
- token-authenticated handshake
- typing and live-update events
- Data
- PostgreSQL 16
- Prisma 7 (`@prisma/adapter-pg`)
- raw SQL for reconciliation jobs
- Storage & media
- S3 API via `@aws-sdk/client-s3` with presigned URLs (`@aws-sdk/s3-request-presigner`)
- self-hosted MinIO
- Sharp
- magic-byte checks on image and audio uploads
- AI / ML
- LLM provider layer (OpenAI, OpenRouter, OpenAI-compatible gateway) for exam grading
- corrections
- translation and message polish; OpenRouter content-safety classifier for moderation; `@vladmandic/face-api` on `@tensorflow/tfjs` + `tfjs-backend-wasm` for face matching; `tinyld` for language detection
- Payments & ads
- RevenueCat (`react-native-purchases`, webhooks, REST reconciliation)
- Google AdMob (`react-native-google-mobile-ads`) with server-side verification of rewarded ads
- Notifications
- Expo push (`expo-notifications`, `expo-server-sdk`)
- Infra
- Docker (multi-stage, Node 24 Alpine)
- Docker Compose
- Caddy reverse proxy
- shell-based watchdog
- health and backup scripts
- EAS Build
- Testing & CI
- Vitest and Supertest against a real PostgreSQL database
- GitHub Actions release pipeline (Gradle bundle, EAS build with TestFlight submit)
Engineering highlights
- Two-limit request throttling with self-healing counters. Concurrency and daily throughput are capped separately so that withdrawing a request and sending it again cannot reset the limit. A nightly SQL job compares the denormalized open-request counter with the actual
PENDING/QUEUEDrows, fixes any difference and logs it.conversa-backend/src/lib/conversationSlots.ts,conversa-backend/src/lib/reconcile.ts,conversa-backend/src/lib/requests.ts - Tiered language matching instead of strict reciprocity. A strict “I teach you, you teach me” rule leaves speakers of less-requested languages with almost no matches. Tier 2 (both people learning the same language) fills that gap, and one-way matches are labelled as such in the UI.
conversa-backend/src/lib/matchTier.ts - Server-side face verification in pure WASM. face-api is loaded through
createRequireto avoid ESM loader failures in tfjs, and it reuses face-api’s own tf instance instead of loading a second copy. Verification runs as an async job after the selfie upload, using a Euclidean-distance threshold on 128-dimensional descriptors.conversa-backend/src/lib/faceVerify.ts - Fail-safe LLM verdicts and quotas. Exam and coach outputs are validated field by field, so a broken response becomes “not passed” or “no correction” and is never treated as a pass. The free-tier translation quota is incremented before the model call, so a request that fails partway cannot bypass the quota.
conversa-backend/src/lib/languageProof.ts,conversa-backend/src/lib/coach.ts,conversa-backend/src/lib/translate.ts - Hardened monetization endpoints. RevenueCat webhooks are checked with a constant-time secret comparison, and consumable packs are credited on the server. Rewarded ads are credited only after AdMob’s ECDSA-signed server-side verification callback. Free users see “who liked you” as tiny, cached, blurred Sharp thumbnails, so the original photo cannot be recovered from the API response.
conversa-backend/src/modules/webhooks/routes.ts,conversa-backend/src/lib/admobSsv.ts,conversa-backend/src/lib/blurThumb.ts
