500+ verified astrologers. 25,000+ monthly active users. 4.6-star average rating across 89% completion. 34% free-trial-to-paid conversion. All running on a single NestJS backend serving three different clients — a Next.js consumer web, a Flutter user app, and a separate Flutter astrologer app — built and shipped in 12 weeks.
That's the headline. Here's the part nobody talks about in case studies.
Booking a verified astrologer in 2023 still felt like 2010. You'd find a number on a Google ad, send a WhatsApp message, get a payment link via DM, dial in to a phone with no recording, no transparency on the per-minute rate, no way to verify the person's credentials, and no recourse if the call dropped halfway through. The Indian astrology market was estimated at over $10 billion. The infrastructure powering it was held together with screenshots and Razorpay payment links.
Hello Astrologer's founders came to us with a clear problem: they had the astrologer network and they had the demand. What they didn't have was the technology layer that lets a first-time user go from signup to a live, billed, recorded consultation in under 90 seconds. They needed a marketplace. Not a slideshow. A real-time, dual-app, voice-and-chat, wallet-billed marketplace.
This is how we built it.
The shape of the problem
Two-sided marketplaces fail in predictable ways. Either the consumer side has no supply, or the supplier side has no demand, or — most common in the Indian context — both sides exist but the friction of the booking flow makes the marketplace economics worse than the off-platform alternative. Why use the app when you can just WhatsApp the astrologer directly?
Every architectural decision we made for Hello Astrologer was reverse-engineered from a single question: how do we make the booked, in-app consultation strictly better than the off-platform WhatsApp call?
That meant five things had to be true:
- Discovery has to be faster than searching Instagram. A user with intent should find the right astrologer — by tradition, language, price, rating, availability — in under 30 seconds.
- The first call has to feel free. Skeptical users don't pay before they've heard a voice. Three free minutes, then auto-charge from a prepaid wallet, with a clear visible counter.
- Audio quality has to beat WhatsApp. This is the make-or-break technical bar. Twilio's voice infrastructure plus a properly tuned client codec was the only way.
- Astrologers have to earn more on-platform than off. The astrologer dashboard has to be so good — automatic scheduling, automatic payouts, automatic recording for dispute resolution — that working off-platform feels like extra work.
- The platform has to bring its own demand. Free Kundli, daily horoscopes, Panchang, Muhurat, matchmaking — every long-tail SEO query a user might type at 11pm with a question they couldn't ask a parent.
Solve all five and the marketplace works. Miss any one and it doesn't.
The architecture, in one paragraph
A unified NestJS backend with GraphQL and WebSocket layers. PostgreSQL for transactional data. Redis for sessions, caching, and the per-minute wallet ledger. Twilio for voice and call recording. S3 for recordings, with a lifecycle policy. Razorpay for wallet top-ups and astrologer payouts. Firebase Cloud Messaging for push. Three clients: a Next.js + TypeScript consumer web (server-side rendered for SEO), a Flutter user app (iOS + Android from one codebase), a Flutter astrologer app (separate app, separate Play Store listing). The whole stack on AWS — EC2, S3, ElastiCache for Redis, RDS for Postgres. As an AWS Partner, we got architecture review hours during the design phase plus startup credits during the launch ramp.
That's the elevator pitch. Now the parts that actually matter.
Why two Flutter apps, not one with role-switching
Free Download: App Development Cost Estimator
Break down app costs by feature — auth, payments, push notifications, maps, chat. See Indian and international rates side by side.
This is the question we get asked the most, so let's address it first.
You can absolutely build a marketplace with one app that switches roles. Uber did it for years. The reasons we chose two separate apps for Hello Astrologer are specific:
App Store and Play Store discovery. A user searching "astrologer" on the Play Store wants to find a consumer app, not a tool that signs them up to become an astrologer. Two listings means two SEO surfaces, two icon designs, two screenshot sets, two review pools. The consumer app gets to look polished and consumer-grade. The astrologer app gets to look like a professional tool.
Permission and binary size. The astrologer app needs background audio, persistent notifications, schedule management, document uploads for KYC, and astrologer-specific calculation tools. The consumer app needs payment, push, and call. Bundling all of that into one binary inflates download size and pushes the Play Store install conversion down — and on a 4G-throttled connection in Tier-2 India, every megabyte costs you.
Codebase isolation. The consumer app changes constantly — UI experiments, pricing changes, promotional banners. The astrologer app is touched less often but its bugs are catastrophic (a missed call notification can lose an astrologer ₹500 of revenue). Keeping them in separate Flutter projects with a shared package for models and API clients lets us ship the consumer app twice a week without going through astrologer-side QA.
Backend simplicity. This is the counterintuitive one. A shared NestJS backend with role-based GraphQL resolvers is much cleaner than a single-app backend with conditional logic on every endpoint. The split happens at the client layer, not the server layer. Both apps hit the same /graphql endpoint and the same WebSocket gateway. The role is in the JWT.
The shared NestJS backend, and why we didn't pick Express
Most Node.js backends in India default to Express because it's what people learn first. For a service that has to expose:
- A GraphQL API (queries, mutations, subscriptions)
- A WebSocket gateway with authenticated rooms
- A REST callback layer for Razorpay and Twilio webhooks
- A scheduled job runner for daily horoscope generation
- A long-running per-minute billing daemon
…Express becomes a pile of boilerplate fast. NestJS gives you dependency injection, modules, guards, interceptors, and a single composition root for all of the above. More importantly, the same BillingService class can be injected into the WebSocket gateway, the REST controller, the GraphQL resolver, and the cron module — without you writing any glue code.
In production, this paid off in two specific ways. First, the BillingService is the single source of truth for per-minute deduction logic. There is exactly one place in the codebase where a wallet balance gets debited. When we needed to add a new pricing tier (premium astrologers at a higher per-minute rate), we changed one method. Second, Nest's testing utilities let us mock Twilio and Razorpay at the DI level, so the entire billing flow runs in a test container in under 4 seconds.
If you're starting a marketplace project today and you're choosing between Express, NestJS, and Fastify — pick NestJS. The 2-week learning curve pays back in month two when you stop fighting your own architecture. We've made the same call on every Flutter + Node app build since.
The real-time consultation engine, end to end
This is the technical centrepiece. Here's what happens when a user taps "Talk to this astrologer" on the discovery screen.
Step 1 — Wallet check. The consumer app calls a GraphQL mutation startConsultation(astrologerId). The resolver checks the user's wallet balance against the astrologer's per-minute rate × a minimum buffer (we use 2 minutes). If the balance is insufficient, the mutation returns a WALLET_INSUFFICIENT error and the app surfaces a top-up sheet.
Step 2 — Astrologer availability. The same mutation hits Redis to check whether the astrologer is currently in another consultation. We use a Redis key per astrologer with a TTL slightly longer than the maximum allowed consultation time. If the astrologer is busy, the user gets a queue position or a "not available" state with the next available slot.
Step 3 — Consultation room creation. A new consultation row is created in PostgreSQL with status PENDING. The room ID is a UUID. The user gets back a WebSocket auth token and the astrologer's app gets a Firebase Cloud Messaging push notification with the same room ID.
Step 4 — WebSocket connection. Both apps connect to the WebSocket gateway and join the room. The gateway is a NestJS module backed by socket.io with Redis as the adapter (so we can horizontally scale across multiple EC2 instances). Authentication happens at the connection layer using the JWT from the GraphQL session.
Step 5 — Free trial timer starts. When both parties join, the consultation status flips to ACTIVE and a 3-minute free-trial timer kicks off, persisted in Redis. The user's app shows a countdown. The astrologer's app shows the timer plus the per-minute rate that will apply after the trial ends.
Step 6 — Per-minute billing kicks in. When the trial expires, a NestJS scheduled task wakes up every 60 seconds and runs the deduction for every active consultation. We use Redis with optimistic locking — a WATCH on the wallet key, then a transactional MULTI/EXEC with the new balance. If the watched key changed (e.g., the user topped up mid-call), we retry the transaction. After three failed retries, we fall back to a Postgres-level row lock. In nine months of production, the fallback has fired 47 times — all during Razorpay webhook bursts after promotional campaigns.
Step 7 — Voice call layer. If the consultation is voice (most are), we generate a Twilio Programmable Voice access token at room-creation time and both apps use it to join a Twilio room. Recording is enabled by default, set to record-from-start. Twilio's recording infrastructure handles the audio capture; we don't touch it.
Step 8 — Recording archival. When the consultation ends — either party hangs up, the wallet balance hits zero, or a maximum duration is reached — Twilio fires a webhook to our REST endpoint with the recording URL. A worker downloads the recording, uploads it to our own S3 bucket under recordings/{astrologerId}/{consultationId}.mp3, and writes the S3 key back to the consultation row. The S3 bucket has a lifecycle policy: anything older than 90 days transitions to S3 Glacier Instant Retrieval. After 365 days, anything not flagged for legal hold gets deleted. Recording storage was the single biggest line item on the AWS bill at 5K MAU; the lifecycle policy cut it by about 73%.
Step 9 — Astrologer earnings. When the consultation closes, the same BillingService method that debited the user's wallet credits the astrologer's earnings ledger — minus the platform commission. The earnings sit in Postgres until the next weekly payout cycle.
Step 10 — Rating and review. Both apps surface a rating prompt at the end of the call. The user rates the astrologer (rating feeds discovery), the astrologer rates the user (used internally to flag abusive callers).
The whole flow above runs in under 4 seconds from "tap" to "voice connected" on a 4G connection. Most of the latency budget is in Twilio's room creation; we trimmed the rest as far as we could.
WebSocket reconnection: the part that almost broke us
In an ideal world, the WebSocket connection stays open for the whole consultation. In the real world, on Indian mobile networks, it doesn't. A user walks under a flyover, switches from 4G to 5G, takes another call — any of these will drop the WebSocket. If the chat history is in-memory only, the message they sent right before the drop is lost forever.
We solved this with a pattern that's worth describing in detail because we've seen it done badly elsewhere.
Every chat message gets a client-generated UUID before it's sent. The client maintains a local outbound queue. When the socket disconnects, queued messages stay queued. When it reconnects, the client replays the queue with the same UUIDs. The server's WebSocket handler is idempotent at the message-UUID level — if it sees a UUID it's already processed, it returns the existing server-side message ID rather than creating a duplicate.
On the inbound side, the client maintains a "last seen server message ID" per room. On reconnect, the first thing the client does is request a delta: "give me everything after server message ID X". The server responds with the missing messages in order.
The combination of idempotent inbound and replayable outbound gives us at-least-once delivery in both directions, with deduplication. We never lose messages and we never show duplicates. This is one of those unglamorous problems that's invisible when it's solved and disastrous when it isn't.
Why a Redis-backed wallet, and what optimistic locking means here
The wallet is the most concurrency-sensitive part of the entire system. A single user can have multiple things happening simultaneously:
- A WebSocket-driven per-minute deduction every 60 seconds
- A Razorpay webhook arriving asynchronously to credit a top-up
- A refund being issued by an admin
- A consultation ending and a partial-minute pro-rata adjustment
If any two of these collide and the wallet update isn't atomic, the user either sees their balance double-charged or double-credited. Both are bad. The double-charge gets a refund request. The double-credit means we're paying astrologers from money we never collected.
Postgres-level row locks would solve this, but at the cost of a Postgres connection sitting open for the duration of every concurrent wallet operation. At 25K MAU with multiple concurrent operations per user during peak hours, that connection pressure was unworkable.
Redis with optimistic locking gave us the throughput. The pattern:
WATCH wallet:{userId}GET wallet:{userId}to read the current balance- Compute the new balance in application code
MULTISET wallet:{userId} {newBalance}EXEC
If anything else SET the wallet key between the WATCH and the EXEC, the EXEC returns nil and we retry. In practice, the retry rate is under 0.3% — Redis is fast enough that the watch window is tiny. The 0.3% that does retry usually succeeds on the first retry.
We mirror every wallet update to Postgres asynchronously through an event log. If Redis crashes (it has, during a failover once), we can rebuild the wallet state from the Postgres event log in about 90 seconds. Redis is the source of truth at the read path; Postgres is the source of truth at the audit path. This split lets us treat Redis as a cache that we can invalidate, not a database we can't lose.
The astrologer side: where the marketplace actually lives or dies
Consumer apps get the design awards. Astrologer apps get the revenue.
If you talk to anyone who has built a two-sided marketplace, the supplier-side experience is what makes the unit economics work. Hello Astrologer's astrologer app has to do five things, all without a support call:
Onboarding. An astrologer downloads the app, signs up with phone OTP, fills in their profile (name, languages, traditions, years of experience), uploads credentials (certificates, ID proof), records a 30-second introduction audio, and sets their per-minute rate. Then they wait for verification.
Verification. Our admin team reviews credentials within 24 business hours. We built a lightweight Directus-backed admin panel for this — every astrologer record has an internal status field (PENDING, APPROVED, REJECTED, SUSPENDED) and a verification notes field. Approval triggers a push notification to the astrologer and a welcome email.
Schedule and rate management. Astrologers set weekly availability windows (Monday 6pm-10pm, Sunday all-day, etc.) plus blackout periods. They can change their per-minute rate independently for chat and voice. Rates take effect at the next consultation; in-flight calls stay at the old rate.
Live consultation handling. When a consultation request arrives, the astrologer app shows a heads-up notification with the user's name, their question (if they typed one), and a 30-second window to accept. Accept and the WebSocket connects; ignore and the request goes to the user's queue position with the next available astrologer in their filter.
Earnings and payouts. The dashboard shows today's earnings, this week's earnings, and lifetime earnings. Each consultation's line-item is broken down: gross amount, platform commission, net to astrologer. Payouts happen every Monday — Razorpay Payouts API runs a sweep across all astrologers with a balance above ₹500 (below that, we roll forward). The choice of weekly over daily was deliberate: daily payouts mean five times the Razorpay payout transaction fees with no meaningful UX benefit.
The single biggest piece of feedback we got from the founding team in user testing: astrologers loved that they didn't have to "log in to a dashboard" to do anything. The app is the dashboard. They run their entire practice from a phone.
The SEO content engine: 60% of consumer-side acquisition
The astrologer app brings the supply. The Next.js consumer web brings the demand. Specifically, it brings the demand at zero marginal cost because the entire content layer is server-rendered and indexed by Google.
The content engine has four pillars:
Daily horoscopes. Twelve zodiac signs, twelve URLs, regenerated every night at 03:00 IST by a NestJS cron job. The generation pipeline pulls the daily Panchang data from a vendor API, runs it through a templated prompt, and writes the output to the database. The Next.js page reads from the database with getStaticProps and revalidate: 3600, so every horoscope is statically generated and cached at the edge but updated within an hour. Search Console impressions on these pages alone account for tens of thousands of organic visits per month.
Panchang and Muhurat. Daily Panchang is a heavy SEO term in India and a high-intent one — people who search "today panchang" are typically planning a specific event. We compute Panchang server-side using astronomical libraries (ephemerides, sunrise/sunset by latitude/longitude). The page is geolocated by the user's IP for accuracy without requiring login. Same SSR + revalidation strategy.
Free Kundli generation. This is the biggest organic acquisition driver. A user submits their birth date, time, and location. We generate a full Vedic birth chart — Lagna, Rashi, Nakshatra, planetary positions, Dasha periods, Sade Sati, common Doshas (Mangal, Kaal Sarp, Pitra). The output is a long, indexable page that lives at a unique URL keyed on the input parameters. We tag the page noindex for the user's specific Kundli (privacy) but the surrounding educational content — what each Dosha means, what Sade Sati is — is fully indexed and ranks for the long-tail terms it targets.
Matchmaking. The Indian Vedic matchmaking system has two main scoring models — Ashtakoot (8 factors, max 36) and Dashkoot (10 factors, max 50). Both involve non-trivial astronomical calculations. We built both engines server-side. A user enters two sets of birth details and gets a compatibility score with detailed breakdowns. Same SEO pattern: the user's specific result is noindex, but the educational content is fully indexed.
The pattern across all four pillars: SSR for Google, dynamic data for users, a thin client-side layer for interactivity. Next.js's hybrid rendering model is unusually well-suited to this; we'd have struggled to get the same SEO outcomes with a SPA.
The infrastructure bill at 25K MAU
Founders ask this question and most case studies dodge it. Here are the actual line items:
- EC2 (3× t3.large for app servers, 1× t3.medium for cron/worker): ~$160/month
- RDS PostgreSQL (db.t3.medium, multi-AZ): ~$110/month
- ElastiCache Redis (cache.t3.medium): ~$55/month
- S3 (mostly recordings, post-lifecycle): ~$45/month
- CloudFront for the Next.js asset CDN: ~$30/month
- Twilio (voice + recording, 25K MAU with avg 4 minutes/user/month): ~$320/month
- Razorpay (per-transaction fees, factored into pricing): ~₹30K/month
- Misc (CloudWatch, SES, Route 53): ~$25/month
That's roughly $750/month plus Razorpay. The Twilio bill is the swing variable; if your call volume spikes, that's where the cost lands. The AWS portion is small enough that it isn't a concern at this scale.
To be clear: this is the bill for the existing 25K MAU. At 100K MAU, with no architecture changes, the AWS bill would roughly triple because we're not yet using auto-scaling groups (we sized boxes for peak headroom). At 250K MAU, we'd need to revisit the WebSocket gateway's Redis adapter for shard-awareness and probably move recordings to S3 Intelligent-Tiering. The current architecture has a comfortable runway to about 500K MAU before any meaningful refactor.
What we'd do differently with hindsight
Three things.
Push notifications via FCM was the right call, but the analytics on FCM are terrible. We should have integrated OneSignal or a similar layer on top from day one. We retro-fitted it in month seven and had to backfill open-rate data manually for the analytics dashboard.
The free Kundli flow is too slow. Generating a full chart with all the Dosha analysis takes around 800ms server-side. At Indian mobile-network speeds, the page feels slow on first load. We should have split the generation into two passes — basic chart in 200ms, full Dosha analysis lazy-loaded after the page paints. We've since done this; on the original launch it was a single render.
The astrologer app's KYC flow had no in-app document scanner. Astrologers were uploading PDF files of their certificates from email. Half of them came in upside-down or out of frame. A Flutter document scanner widget (we used flutter_doc_scanner in a later release) would have saved the admin team about four hours a day in verification work in the first three months.
Why this case study matters if you're not building an astrology app
Strip away the surface and Hello Astrologer is a real-time per-minute consultation marketplace. The same architecture has shipped, in our hands and in others', for:
- Online medical consultation (Practo's category, but for tier-2 cities)
- Legal consultation (lawyer matching by specialisation)
- Personal training and fitness coaching (per-minute video sessions)
- Astrology-adjacent services like tarot and numerology
- Live tutoring (homework help, language coaching)
The unit economics shift with the per-minute rate, but the technical surface is the same. WebSocket for chat. Twilio (or its alternatives) for voice. Per-minute wallet billing. Two apps. SEO-driven content for organic acquisition.
If you're thinking about a marketplace and you're stuck on the technology choice, the lesson from Hello Astrologer is: marketplace economics are decided by the supplier-side experience. Build the supplier app first. Build the consumer app second. Build the SEO engine third. In our 12-week build, we spent the first 3 weeks on the astrologer app, weeks 4-7 on the consumer app and shared backend, weeks 8-10 on the SEO content engine, and weeks 11-12 on hardening and launch.
We're a DPIIT-recognised app development team that's built marketplaces in this exact shape. You can see two more here: Arre Voice — a Flutter social audio platform where the real-time room model was the hard part, and Best Wallet — a Flutter crypto wallet where we learned a lot about state machines and recovery semantics that fed into the wallet design here. The full Hello Astrologer write-up sits in our portfolio. For how we typically run a build, see how we work.
Free Download: App Development Cost Estimator
Break down app costs by feature — auth, payments, push notifications, maps, chat. See Indian and international rates side by side.
Written by

Founder & CEO
Rishabh Sethia is the founder and CEO of Innovatrix Infotech, a Kolkata-based digital engineering agency. He leads a team that delivers web development, mobile apps, Shopify stores, and AI automation for startups and SMBs across India and beyond.
Connect on LinkedIn