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.
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.
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
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