Soulmate

full-stack matrimonial platform

Soulmate — Where Intentions Meet Forever

A paid matchmaking app where double-credits and double-charges are ruled out by schema constraints.

role
full-stack developer
status
● live demo
scale
20 search filters
stack
Next.js · NestJS · PostgreSQL · Socket.IO
type
live demo · closed source

FIG.01 // SOULMATE · CREDITS

The bugs that cost real money are ruled out by the schema

A double-fired payment cannot credit twice, and every spend is a guarded decrement. For nudges and read receipts, the charge and the thing it pays for commit together.

The bugs that cost real money are ruled out by the schema01 · RAZORPAYcheckoutCLIENTverify callHMAC signatureSERVERwebhookHMAC · timingSafeEqual02 · CREDITaddCreditsincrement + row03 · PARTIAL UNIQUE INDEX1 credit per payment idONE INSERT WINS04 · SPENDpaid actionnudge · read receiptONE TRANSACTION · THE CALLER'SCONDITIONAL DECREMENTcredits ≥ costchecked at write timeSAME TXeffect rowthe thing it pays forOUTCOMEcommit / rollbackboth or neitherSIDE · DISCOVERYshown-profiles ledgerunique (user, profile)both may fire for one paymenta concurrent second insert fails;its increment rolls back with itfor these two, the charge commits with what it buysnext page excludes what was shown: freea re-filter is a fresh search: billed

A matrimonial platform on NestJS and Next.js 15 over a single PostgreSQL backend: realtime chat through a Socket.IO gateway, an interest → accept → connect matchmaking loop, a Razorpay-powered credit economy and a 20-filter discovery engine. Once an action costs a credit and a credit costs a rupee, a double-fired webhook is a free top-up and a retried request is a double charge, so those guarantees are pushed into the database schema.

01

A payment is credited at most once

After a Razorpay checkout, two paths can credit the same payment: the client's verify call and the server webhook. Both recompute an HMAC-SHA256 signature before anything is believed, and the webhook compares it with crypto.timingSafeEqual rather than string equality.

Both then reach addCredits. A check in the handler alone would let two concurrent requests pass together, so the guarantee is a partial unique index on the transactions table, covering gateway_transaction_id where the row is a credit and the id is not null. The second insert cannot succeed, and because the atomic increment runs in the same transaction, it rolls back with it.

partial unique indexHMAC-SHA256one insert wins

02

A charge commits with what it buys

Spending is a guarded conditional decrement: the update only matches while credits ≥ cost at write time, so two concurrent spends cannot both pass and the balance can never go negative. Each debit records balance_before and balance_after as decimals, not floats.

For a nudge or a read receipt, the spend runs inside the caller's transaction, so the deduction and the action it pays for commit or roll back together. Message and search spends use the same guarded decrement in a transaction of their own.

Discovery follows the same rule of paying once for one result. Every profile surfaced to a user is written to a ledger keyed unique on the pair of user and profile, so pagination is free and a re-filter is a fresh, billed search.

credits ≥ cost at write timeone transaction

screens