01 / Case Study
Creator Subscription & Wallet Platform
Real-time messaging, subscription lifecycles, and a multi-provider wallet and ledger, run across a horizontally scaled NestJS monorepo.
The problem
The platform sells subscriptions and pays out creator earnings, so two things have to be true at once: chat and notifications need to feel instant across many server instances, and every rupee moving through checkout, renewal, or payout needs to land exactly once — no duplicate charges, no dropped renewals, no earnings that vanish on a retry.
Architecture
The back end is a NestJS 10 monorepo: five deployable apps — core, admin, agency, auth-server, notifications — over seven shared libraries covering auth (JWT per audience plus CASL policy guards), wallet and transaction handling, agency-side transactions, PDF generation, and signed CDN asset URLs. MongoDB (Mongoose 7, snake_case fields, additive-only schema changes — no migration framework) holds the data; Redis and BullMQ carry every background job. Roughly 28 domain modules live under core — posts, stories, channels, groups, feeds, shops, payouts, moderation, complaints, a partner API lane, and payment tracing among them — which is a lot of surface for one service, so the boundaries that matter are the ones drawn around money, media, and real-time rather than around individual features.
Real-time
Sockets are authenticated at the handshake, before a connection can join any room — an unauthenticated socket never reaches a handler. Beyond that boundary sit two stateful handlers, both keeping their state in Redis rather than in process memory, because any instance may serve any user.
- —Presence tracks multiple concurrent sessions per user — the same account on a phone and a laptop is one presence, not two — across online-public, online-private and live states, with a preference to hide status entirely. Sessions are re-synced and verified on boot so a restarted instance doesn't leave ghosts online.
- —Live streams hold a viewer set per stream, aggregate reactions through a throttle so a burst of taps doesn't become a burst of emits, and expire on two timers: an idle auto-expire and a longer graceful window for a stream that drops mid-broadcast.
- —The Redis adapter republishes every emit to sibling instances, so a user connected to one server receives events raised on another. That is what makes the socket tier horizontally scalable instead of sticky.
- —Notifications branch two ways from the same event: Firebase multicast for device push, and a persisted store backing in-app read/unread counts and acknowledgement.
Async & queues
Every slow or failure-prone operation runs on BullMQ rather than in the request path. The topology is deliberately wide — twelve-plus dedicated queues rather than one general worker — because the queues are separated by failure domain, not by convenience.
- —Media: asset-optimizer and a separate image/audio lane, so a large video transcode can't sit in front of an avatar resize.
- —Money: wallet transactions on their own queue, isolated from anything that could back it up.
- —Messaging: message, response, and unread-message-count queues on a separate Redis connection from the main lane.
- —Platform: event bridge, scheduled task, channel, and cron; plus security-email and account-email split apart so a marketing backlog can never delay a password-reset mail.
- —The reason for the split is operational: a stuck media job must not delay a subscription renewal. One shared worker pool makes that failure mode unavoidable; separate queues make it a non-event.
Decisions
Idempotency at the money boundary
Checkout unifies five payment paths — card, two SEPA/iDEAL providers, a legacy gateway, and an internal wallet — behind one endpoint, with each provider's webhook signature-verified independently before it can touch a transaction. Renewals and retries are guarded so a webhook replay or a retried job can never double-charge or double-credit.
Wallet as two buckets, not one
Spendable balance and locked (escrowed) creator earnings are separate fields, moved between by a scheduled release job rather than by application logic reaching into both. That separation is what makes a partial refund, a chargeback, or a wallet-first purchase safe to reason about independently.
Queues own everything slow
Renewal processing, invoice generation, and notification delivery all run through BullMQ rather than inline in the request path, so a slow downstream call (a payment provider, an email service) never holds open an HTTP response.