Full-Stack Matrimonial Platform

Soulmate:
Where intentions meet forever

A production matrimonial platform — realtime chat, a complete interest → accept → connect matchmaking loop, a Razorpay-powered credit economy, and a 20-filter discovery engine, all on a single PostgreSQL backend.

20 search filtersRazorpay credit economySocket.io realtime chat
LIVE DEMO
Try the live demo
email male1.user@example.com · password Password123!

TECHNICAL HIGHLIGHTS

Realtime Chat

Socket.io messaging with presence and multi-device sockets

Matchmaking Loop

Interest → accept → connect → chat state machine

Credits Economy

Razorpay top-ups + daily free-search/message allowance

Per-Request Discovery

20+ filters sent per search — change a filter, get new results

Correct by Construction

Idempotent payments, guarded credit spends, atomic writes

Notifications

In-app notification centre + Firebase push (FCM)

Visual Tour

Explore the platform's features through detailed screenshots.

Curated Discovery
Discovery

Curated Discovery

20-Filter Search Panel
Discovery

20-Filter Search Panel

Rich Match Profiles
Profiles

Rich Match Profiles

Razorpay Credit Wallet
Payments

Razorpay Credit Wallet

Warm, Premium Landing
Experience

Warm, Premium Landing

Account & Privacy
Account

Account & Privacy

Correctness

The bugs that cost real money

Once an action costs a credit and a credit costs a rupee, a class of bug stops being cosmetic. A double-fired webhook is a free top-up. A retried request is a double charge. The pattern used throughout is to push these guarantees into the database schema, where they hold regardless of which code path arrived and how many times.

FOUR GUARANTEES

A payment can be credited at most once

A partial unique index on the transactions table covers gateway_transaction_id where the row is a credit and the id is not null. A replayed Razorpay webhook does not double-credit a wallet — not because the handler checks, but because the insert cannot succeed twice.

Interest is a unique pair, not an event log

matrimonial_connections is keyed unique on (requester, recipient). Sending interest is an upsert back to 'pending', so tapping the button twice cannot produce two requests, and the pair's state is one row rather than something reconstructed from history.

A webhook is verified before it is believed

The Razorpay signature is recomputed as an HMAC-SHA256 over the raw body and compared with crypto.timingSafeEqual rather than string equality, so the comparison does not leak information through how long it takes to fail.

Prices are validated against the ledger, not the request

A payment is checked against an allow-list of permitted rupee amounts and a permitted credits-per-rupee band before anything is written. A client asking for a thousand credits for one rupee is rejected on arithmetic, not on trust.

FIVE FRAUD CHECKS ON A SPEND

velocityamount anomalysuspicious patterngeographicdevice

The detector returns a list of typed alerts rather than a boolean. Each carries a severity, a human description and an evidence object, so a flagged spend can be reviewed rather than only blocked — which matters because every one of these checks has false positives and a paying user hitting a silent wall is its own kind of failure.

The velocity check reads the last hour of debits and compares both cumulative amount and transaction count against thresholds set from what normal use on this platform looks like. The anomaly check reads thirty days of history to establish that baseline per user rather than globally.

Transactions record balance_before and balance_after as decimals, not floats. The ledger is reconstructable and it does not drift.

Discovery

A search you paid for stays paid for

If a search costs credits, showing someone the same twelve profiles twice is charging them twice for one result. So every profile surfaced to a user is written to a ledger table keyed unique on the pair of user and profile, and a search can ask to exclude everything already on it. Pagination is free and re-filtering is a fresh search — the boundary between the two is what the pricing hangs on.

What a user is looking for is itself a row. The saved-preference table carries twenty-seven criteria columns across partner basics, location, education and profession, lifestyle, family, and languages — most of them JSON arrays, because “any of these three religions” is a normal thing to want and a single-value column cannot express it. The search panel opens pre-filled from that row, so the default search is the one the user already described.

Free and paid tiers are configuration rather than code. A platform_limits row holds the daily free allowances and the price of each paid action — searches, messages, read receipts, nudges, unlocking a reply to an interest. Repricing the product is an update statement. Consumption is counted in a per-user, per-day row with a unique constraint on that pair, and a midnight cron closes off the previous day.

RATE LIMITS SCALE WITH WHAT A ROUTE COSTS

Three global tiers apply to everything — 12 requests a second, 80 in ten seconds, 400 a minute — which is a burst guard, not a security control. The routes where abuse is actually expensive override it, and the limits get much tighter the closer a route gets to sending mail, creating an account, or moving money.

Email OTP resend

2 per 10 minutes

Password reset request

3 per hour

Register

3 per minute

Login

5 per minute

Spend credits

10 per minute

Profile search

12 per 6 seconds

Two per ten minutes on an OTP resend is deliberately frustrating. It is also the difference between an email-sending endpoint and a free spam relay, and it is the kind of limit that has to be set before it is needed rather than after.

Caching is optional by design. Redis is used when a URL is configured, with TLS forced for managed hosts and connection errors logged rather than allowed to take the process down; with no Redis configured the app falls back to an in-memory store and boots normally. A cache outage is a slower platform, not an unavailable one.

The loop

Interest, accepted, then a conversation

Twenty-five Drizzle tables sit behind this, but the product is really one state machine: a connection row between two users that is pending, accepted or declined, and a set of capabilities that unlock at each step. A profile page reads that row and changes what its action bar even offers — Show Interest before, Message after. Accepting stamps a response time and fires a notification that says in as many words that the two of them can now message each other.

Blocking is checked in both directions before any of it. A block by either party removes the other from discovery and closes the connection path, which is a single bidirectional check rather than a rule each feature has to remember to apply. Contact details are never on a profile at any state — the product is built so that reaching someone is always mediated by the platform, which is both a safety property and the thing the credit economy is attached to.

Chat runs as a Socket.IO gateway inside the same NestJS process rather than as a separate realtime service — one deployment, one auth path, one database connection pool. Devices register FCM tokens over that same socket, so a message delivers to an open tab if there is one and to a push notification if there is not, without the sending code branching on which.

TECHNOLOGY STACK

frontend

  • Next.js 15

    App Router, React 19

  • TypeScript

    Type-safe end to end

  • Tailwind CSS v4

    Warm, custom design system

  • shadcn / Radix UI

    Accessible primitives

  • TanStack Query

    Server-state & caching

  • Zustand

    Client/realtime state

  • Socket.io client

    Realtime chat & presence

backend

  • NestJS 11

    Modular, guarded API

  • Drizzle ORM

    Single PostgreSQL, typed SQL

  • PostgreSQL

    One consolidated database

  • JWT Auth

    Sessions + refresh tokens

  • Socket.io Gateway

    Chat on the main server

  • Razorpay

    Payments & webhooks

  • Cloudinary / Redis

    Media + Upstash cache

devops

  • Render

    Backend hosting

  • Vercel

    Frontend hosting

  • Firebase FCM

    Push notifications

  • drizzle-kit push

    Schema reconciled on deploy