Case study · Healthcare · Marketplace

A multi-tenant healthcare marketplace, from first commit to live beta.

Hospitals in India onboard their doctors, and patients book, pay for and join video consultations with them. Metageeks built the web app, the API and an Android app between May and September 2026. The web app is live in beta.

  • Web application
  • Mobile app
  • Multi-tenant SaaS
Doctor on a video consultation at a laptop in a clinic, for a multi-tenant healthcare marketplace case study in India
Client
A multi-vertical healthcare marketplace, India
Status
Beta: web live, Android in internal testing
Built
May to September 2026
Platforms
Web and Android
Stack
Next.js, NestJS, PostgreSQL, Flutter
Services
Web application development, Mobile app development, Custom software development
4
user types, from patient to platform admin
6
tenant types modelled, hospitals first
8
days from first commit to a working patient flow
1,100+
automated tests across web, API and mobile

Summary

The platform in brief.

The platform is a multi-tenant healthcare marketplace that lets hospitals in India onboard their doctors and lets patients find a doctor by specialty, book a slot, pay with Razorpay and join a video consultation. It is built to hold six kinds of tenant: hospitals, psychology clinics, IVF clinics, law firms, detective agencies and e-commerce vendors. Hospitals come first, and the other five are modelled for a later phase.

Every tenant's data sits in one PostgreSQL database, and row-level security keeps each tenant's rows out of every other tenant's queries. Patients prove their identity through DigiLocker, and their Aadhaar number never reaches the API. Doctors are checked against the national medical registry. Metageeks built the Next.js web app, the NestJS API and a Flutter Android app between May and September 2026. The web app is live in beta, and the Android app is in internal testing.

The brief

One platform, many tenants, none of them able to see each other.

The client set out to run a healthcare and lifestyle marketplace for India, hosted in the AWS Mumbai region. Hospitals join as tenants and bring their own doctors. Patients find a doctor by specialty, book a slot and pay online.

Four kinds of user share the platform: patients, doctors, the admins and staff of each tenant organisation, and a platform super admin. The data model also defines five tenant types beyond hospitals: psychology clinics, IVF clinics, law firms, detective agencies and e-commerce vendors. Those verticals share the same core, so the boundary between tenants had to hold for all of them.

The challenge

Identity, payments and video, all regulated.

Every party on the platform has to be verified. Patients prove who they are with Aadhaar through DigiLocker. Hospitals are checked as companies against their CIN, GSTIN or MSME registration. Doctors are verified against the national medical registry before they can accept patients.

Payments run through Razorpay and consultations happen over live video on LiveKit. Every booking, payment and consultation belongs to one hospital and has to stay invisible to the others.

The launch also had to stay lean, with one API, one web app and one database to deploy and run.

The approach

A modular monolith with the tenant boundary in the database.

Metageeks built one NestJS 11 application that serves the API and runs the background workers, split into modules whose boundaries CI checks with dependency-cruiser. A single Next.js 16 app serves the web. Prisma talks to PostgreSQL 16, Better Auth handles sign-in and CASL decides what each role may do.

Every domain table carries a tenantId column, and PostgreSQL row-level security (RLS) returns only the rows for the tenant set on the current transaction. Slow work, such as checking a doctor against the medical registry, runs as BullMQ jobs on Redis. The Android app is written in Flutter and calls the same API with bearer tokens.

Architecture diagram of the multi-tenant healthcare marketplace: web and Android apps, a NestJS API, BullMQ workers and PostgreSQL with row-level security
Both apps call one API. Workers take slow jobs off the request path, and PostgreSQL enforces the tenant boundary on every query. Open full size ↗

For patients

From sign-up to a video consultation.

A patient signs up with a password, a magic link, a phone OTP or a Google account, then completes an identity check. They find a doctor by specialty, see the doctor's fee and next available slot, and book. Booking stays locked until the identity check is done.

Payment goes through Razorpay. At the booked time, patient and doctor join a LiveKit video room. The first working version of this flow, from sign-up through identity check, doctor search and booking to the video call, ran eight days after the first commit.

For hospitals and doctors

Onboarding a hospital, verifying a doctor.

A hospital joins through an email invite. Its company details are checked against CIN, GSTIN or MSME records, and its admin signs the platform agreement electronically. The admin can invite staff and doctors before the company details are complete, and each invite link works once.

A doctor who joins enters their medical registration number. The platform checks it against the national medical registry as a background job, and the doctor can accept patients once it passes.

Patient identity runs through DigiLocker, so the raw Aadhaar number never reaches the API. The server ignores the result the app reports and fetches the record from the KYC provider itself. A webhook, a finalize call and polling can each settle the check, and whichever arrives first wins. The stored record is encrypted with AES-256-GCM under rotatable keys and purged after a retention period.

The key decision

Isolation enforced where it cannot be forgotten.

The most important decision was where the tenant boundary lives. If application code filters by tenant, every query has to remember its WHERE clause, and one forgotten filter shows a hospital another hospital's records. Metageeks put the boundary in PostgreSQL: row-level security policies on each tenant table return only the rows for the tenant set on the current transaction.

Before launch, Metageeks found and fixed a gap in that design. On a pooled connection that had been used before, the tenant setting fell back to an empty string, and the policies read an empty string as the super admin bypass. The fix made the bypass an explicit sentinel value, extended RLS to 8 more tables and wrapped the 12 queries that depended on the old behaviour.

A review of the access-control path found 13 defects, 6 of them high severity. An invite with no role granted owner, some controllers had no guard, and the request body could override the session's tenant. The fix introduced a multi-role model, a partial unique index that lets a user be admin in only one organisation, and roles scoped to the transport, so a mobile session drops roles meant for the browser.

Diagram of tenant isolation in the healthcare marketplace: the tenant travels from the session to PostgreSQL row-level security, with the explicit bypass sentinel fix
The tenant comes from the session, is set on the transaction and is enforced by PostgreSQL. The bypass now needs an explicit sentinel. Open full size ↗

Access

Four kinds of user, each with its own limits.

CASL rules decide what each role can do, and the API checks them on requests. Row-level security sits underneath, so a query inside one tenant's transaction cannot return another tenant's rows.

Patient

Signs up, completes the identity check, books and pays for consultations and joins the video call.

Doctor

Joins through a hospital invite, verifies their medical registration and runs video consultations.

Hospital admin

Completes the company checks and the e-signed agreement, then invites staff and doctors.

Platform admin

Approves tenants and reviews KYC. Signs in with TOTP, and support access into a tenant is time-limited and audited.

Technical detail

Under the hood.

The technical choices behind the platform, and what each one does.

PostgreSQL row-level security
Every domain table carries tenantId. Policies return only the current tenant's rows, and the super admin bypass needs an explicit sentinel value.
CASL authorization
Role rules checked by the API, with roles scoped to the transport so a mobile session carries only mobile roles.
Better Auth
Password, magic link, phone OTP and Google sign-in, two-factor authentication, and bearer tokens for the Android app.
AES-256-GCM payload encryption
KYC payloads are sealed with a key identifier, so keys rotate without re-encrypting old records, and are purged after a retention period.
BullMQ workers
Registry checks and other slow jobs run as queued jobs on Redis, in the same NestJS codebase as the API.
CI against real PostgreSQL
Pushes to the main branches build, lint and run the tests against PostgreSQL with RLS policies applied, then check module boundaries with dependency-cruiser.
Flutter mobile app
An Android-first app for patients and doctors, on the same API as the web app.
AWS hosting
The web app runs on AWS Amplify, and the API ships as a Docker image to EC2 in the Mumbai region.

Outcome

Where it stands.

The web app is live as a beta. Payments and KYC still run on test and sandbox keys, and the Android app is in internal testing.

Hospitals are the first tenant type. Psychology clinics, IVF clinics, law firms, detective agencies and e-commerce vendors are modelled on the same core for a later phase. An AI assistant is planned and has not been built.

Good fit

For teams building something regulated.

The same approach suits platforms where several organisations share one product and their data must never mix.

  • Several organisations share one product, and each must see only its own data
  • Users prove who they are with Aadhaar, company or professional registrations
  • Payments and live video sit inside the core flow
  • The product launches with one vertical and has more planned

Real questions

Common questions

How do you keep one tenant's data away from another's?+

Every tenant table carries a tenant ID, and PostgreSQL row-level security returns only the rows for the tenant set on the current transaction. The tenant comes from the signed-in session, never from the request body. On this platform the super admin bypass also needs an explicit sentinel value, so an empty setting cannot open every tenant's rows.

How does Aadhaar-based KYC work without storing Aadhaar numbers?+

The patient completes the check inside DigiLocker, so the raw Aadhaar number never reaches the platform's API. The server then fetches the verified record from the KYC provider itself instead of trusting the app, stores it encrypted with AES-256-GCM and purges the stored payload after a retention period.

Can one codebase serve hospitals and other verticals?+

Yes. This platform's data model defines six tenant types: hospitals, psychology clinics, IVF clinics, law firms, detective agencies and e-commerce vendors. Hospitals are onboarded first, and the other five are planned for a later phase on the same API, database and tenant isolation.

Do clients own the code Metageeks writes?+

Yes. Our engagements end with the source code handed over, so you can run it, change it or bring in another team without asking us.

Your platform

Tell us who needs to use it.

Describe the organisations, users and checks your product has to handle, and we will tell you how we would build it and what it would take.

Related services

All case studies Web application development Mobile app development Custom software development Tell us what you need