Work · 03 of 9

Humraah

Matrimonial platform. Backend, PWA and mobile app taken from a failed audit to store review

Role
Sole engineer on backend, PWA and mobile (Third Shade Media)
Team
Shreya (8 commits)
Studio
Third Shade Media Services
Period
Jun to Sep 2026
Status
store review
Stack
  • Node.js
  • Express 5
  • MongoDB
  • JWT
  • Expo
  • React Native
  • TypeScript
  • Cloudinary
  • Razorpay
  • Apple IAP
  • Google Play Billing
  • FCM
  • APNs
  • Gallabox
  • SurePass
  • Render
Links

Measured

MetricValueWindowSource
Blocking audit findings, 35 fixed, 5 accepted as non-blocking; localStorage auth, public image URLs, missing spec features40 → 030 Jun to 12 SepAll_Issues.md and launch QA register F-01…F-30, 12 Sep 2026
Backend commits, the rest by one teammate355 of 363Jun to Sep 2026git shortlog -sn, humraah-backend, 12 Sep 2026
Mobile commits, Expo app built from scratch155 of 155Jul to Sep 2026git shortlog -sn, humraah-mobile, 12 Sep 2026
Pre-registrations, founding-member list before launch500+Sep 2026Third Shade, 14 Sep 2026

Problem

Humraah is a private, family-first matrimonial service: Aadhaar-verified profiles, up to three curated introductions a week, photos hidden until the chat stage, a five-day supervised family chat, and a guarded path from YES/NO/LATER to mutual interest, biodata and a Meet/No decision. When I joined in late June the product looked finished from the outside. An audit on 30 June found forty issues, including auth that lived in localStorage, user images on public URLs, and features from the spec that did not exist. The brief became: make it real, then take it to the app stores, on one backend.

This is client work for Third Shade Media, so there are no product screenshots here. What follows is the architecture, the code, and the decisions, all of which are mine to show.

From the repository

518Commits510 mine, 98%
26,054Lines of code200 source files
8Test filesin the tree
Counted from humraah-backend + humraah-mobile on 2026-09-15, commits 2026-06-17 to 2026-09-12. Source lines only (no vendored, generated or built files). Backend and mobile repositories together; one teammate contributed 8 backend commits.

Express 5 on Node with Mongoose 9: 13 route files, 23 services, 26 models, eight node:test suites. The Expo app is TypeScript with expo-router, SecureStore for tokens, and expo-screen-capture so chat and biodata screens cannot be screenshotted. Thirteen external integrations: SurePass for Aadhaar and DigiLocker, Gallabox for WhatsApp templates and the bot, Razorpay on web plus Apple IAP and Google Play Billing in the app, Firebase and APNs for push, Resend for email, Cloudinary for private media, Google Places with an OLA Maps fallback, Groq for the FAQ chatbot, Tesseract and pdf-parse for biodata extraction.

One backend, three clients

WordPress + PWAservice worker, web pushExpo appAndroid, iOSAdmin dashboardmoderation, analyticsNode / Express backend13 routes · 23 services · 26modelsIntegrationsSurePass, Gallabox,Razorpay, IAP, FCM, APNsMongoDB, Cloudinaryprivate media
Public pages and the PWA on WordPress and Hostinger; the app on Expo; everything on one Node backend.

The PWA is plain HTML, CSS and JavaScript served from Hostinger rather than from WordPress itself, after plugin conflicts kept breaking the app pages. The app was rebuilt screen by screen against the website as the source of truth, which surfaced a whole class of React-Native-Web bugs (auth headers on images, Alert callbacks, FormData) that the web client never had. The backend does not know which client is calling except through an ApiMetric middleware that records platform and UTM for the admin analytics.

What I built, in order

  • Security first (30 June to 5 July). Strict JWT auth replacing the localStorage token, helmet, hpp, mongo-sanitize, rate limiting; user images moved to private Cloudinary behind an authenticated route; admin auth hardened; release builds stopped logging profile status.
  • Matching. Compatibility scoring, family-exclusion rules so members of one family group are never matched (with FamilyGroup, FamilyExposure and FamilyEvidence models and a test script), pair eligibility, separate pools for first marriages and new journeys, a daily match-feed job.
  • Registration. WhatsApp OTP, resumable drafts, a gender-based photo policy with on-device face detection (MediaPipe), Google Places addresses proxied and cached server-side, Aadhaar via SurePass with a deep-link return for native and duplicate-Aadhaar recovery, payment gate, email verification.
  • The notification policy engine. Twenty-five event types over four channels with opposite eligibility rules. One policy file decides; every channel enforces it a second time. More below.
  • Moderation and the match journey. A four-level moderation engine scoring thirteen violation classes, report and block with a permanent-block privacy model, the five-day supervised chat with photo-request gating, and the Meet/No decision after chat expiry.
  • The app (24 July to 12 September). Expo and React Native from scratch: design system, every screen, push end to end with platform-aware payloads and a 28-entry deep-link table, Aadhaar on device, Play Store UGC compliance (reporting, blocking, moderation), a reviewer sign-in path, in-app purchases on both stores.

The notification policy, and the message that could never send

Three delivery channels, three opposite rules. WhatsApp is for members who are not yet live: it carries the six registration-recovery reminders whether or not push works, because a family that has not installed the app still needs to be told their payment is due. Email is for members who are live, and only to an address they verified, so an optional email can never become a registration drop-off. Push goes wherever a token exists. Members can mute intros, mutual interest or chat, and the mute is a substring match on the type name.

That last detail is how I found the first bug: muting chat also silenced chat_closed_decision and chat_meet_confirmed, the two moments the whole five-day window exists to produce. The second bug was structural. profile_verified, the “your profile is now live” message, was only fired when the status was active, and the WhatsApp gate required the status not to be active. An approved template with one variable, configured, and unreachable for every member by construction. payment_success and profile_rejected had the same shape: a member pays ₹499 and, without a push token, is told nothing on any channel.

The fix is three explicit allow-lists in notificationPolicy.js with the reasoning written above each one, and a flag threaded through to the WhatsApp sender, which re-checks the status on its own and would otherwise drop the message the policy had just allowed.

Notification policy explorernotificationPolicy.js, 25 types × 4 channels
ChannelResultWhy
in-appsentAlways written to the notification centre; the audit log records every channel attempt, including the ones skipped by policy.
pushFCM_NO_TOKENNo FCM token registered for this member (no app install, or notifications refused).
whatsappsentNamed post-activation exception. The policy answer is passed to sendWhatsAppTemplate as allowPostActivation, because that function re-checks the status on its own and would otherwise drop it.
emailEMAIL_NOT_VERIFIEDThe notification address exists but was never verified; registration stores it with notificationEmailVerified = false.

profile_verified is category system; besides the notification centre it reaches 1 channel.

Timeline

  1. First backend commit
  2. Audit: forty issues, localStorage auth, public images 22 commits that day start the security rebuild.
  3. Security rebuild lands JWT, helmet, hpp, mongo-sanitize, rate limits, private media; 35 commits.
  4. Match journey: mutual interest, biodata, Meet/No decision 36 commits.
  5. Expo project initialised the mobile app starts from zero.
  6. First device build payments, push and Aadhaar verified on real hardware.
  7. Play Store readiness UGC policy: reporting, blocking, moderation.
  8. Store compliance sprint reviewer sign-in, IAP on both stores, privacy declarations; 46 commits.
  9. Handover repositories moved to the company org with full history; Android on internal testing, iOS in TestFlight.
From both repositories' git logs and the daily work log. 518 commits over 88 days; the busiest single day was 9 September with 46 backend commits for store compliance.

Before and after

BeforeAfter
AuthToken in localStorage, no expiryJWT with role checks, rate-limited OTP, hardened admin
Member photosPublic Cloudinary URLsPrivate storage, served through the API
A single reportSuspended the account, killed chats and matching, no restoreTwo distinct reporters required; automatic reinstatement on dismissal
"Profile verified" messageCould never send: one guard required active, the other required not activeExplicit post-activation allow-list; the template fires
Muting chatAlso muted the decision-due and contact-shared emailsTwo mute-exempt email types; push stays muted
ClientsWeb onlyWeb, PWA, Android on internal testing, iOS on TestFlight

Where it stands

Google Play: bundle 1.0.0 (5) on internal testing, listing and every content declaration done, the ₹499 one-time product active in 169 regions; the only step left is finance attaching a bank account and pressing rollout. App Store: build 1.0 (8) in TestFlight, version page and App Privacy complete, blocked only on the Paid Apps Agreement. Thirty-five of forty QA register items fixed, the rest documented as non-blocking.

What I would do differently: the chat is REST polling every four seconds rather than a socket, and the cron jobs run inside the API process, which is fine for one Render instance and wrong for two. Both are on the handover list, with the reasoning.

Learned: One backend, three clients, and every notification channel has its own rules.

keysKeyboard
j / k
next / previous row
Enter
open the focused row
⌘K or /
search, or ask this site
g then h w a c
go home, work, about, contact
t
toggle light and dark
Esc
close the palette or this map
?
this map