Public landing page for hospitals, pharmacies and patients. Hospital admin dashboard with appointment trends and quick actions. Shown with demo data. Appointment management across the full clinic lifecycle, filterable by doctor, patient and nurse. Demo data. PharmaStock: one public catalog across every approved pharmacy. Demo data with placeholder product art. SuperAdmin product moderation with per-pharmacy auto-approve and bulk review. Demo data.
On this page
The Problem
Healthcare software is usually sold in silos. A clinic buys an appointment system. A pharmacy buys an inventory or POS tool. Patients get neither: they phone the clinic to book, and walk into pharmacies to find out whether a prescribed medicine is even in stock.
The client wanted all three audiences on one platform, which brought a set of hard requirements that never show up in a single-tenant product:
- One codebase, four different products. A hospital admin, a doctor, a patient and a pharmacy owner need completely different navigation, data and permissions, yet they share accounts, organizations and a medicine catalog. A patient can also be a customer; a doctor prescribes a drug a pharmacy sells.
- Hard tenant isolation. Hospital A must never see Hospital B's patients. This is patient data, so a leak is an incident, not a bug. Isolation had to be structural, not a filter someone remembers to add.
- Inventory that cannot lie. A public pharmacy storefront means concurrent checkouts against the same stock. The naive "read quantity, subtract, write back" silently loses updates and oversells: you sell 10 units of a drug you have 6 of, and find out when the customer arrives.
- Booking that respects real clocks. A doctor's availability is a weekly schedule in the clinic's timezone, but appointments are instants. Get that wrong and you double-book across a daylight-saving boundary.
- An operator who can intervene. Pharmacies sell regulated products. Someone has to vet a pharmacy before it goes live, moderate what it lists and suspend it, with an audit trail.
What I Built
A multi-tenant SaaS where hospitals, pharmacies and patients each get their own product experience on one codebase, one database and one identity system, plus a SuperAdmin control plane for the platform operator. Built with Next.js 16 (App Router, Server Components, Server Actions), React 19, TypeScript and MongoDB.
Multi-tenant identity and RBAC
Tenancy is a three-part model rather than a single tenant column: a tenant-agnostic User, a Membership (user × organization × role, uniquely indexed) and an org-scoped member profile for employment data such as licence number, session rate and employee ID. One person can belong to several organizations with a different role in each, and switching organization re-issues the session with a fresh tenant context.
Organizations are typed (hospital, pharmacy, platform) and carry their own lifecycle, from pending and under review to approved, suspended or permanently banned. Permissions are a capability matrix (prescriptions:write, patients:read, team:write, billing:read and so on) mapped to roles: Admin, Doctor, Nurse and Patient.
Enforcement is layered on purpose, because each layer catches what the one above it cannot:
- Edge middleware routes each user to the right dashboard from session claims: fast, but knowingly stale
- Server layouts re-check the organization's approval status live, catching a suspension that happened after the token was issued
- Server actions call
authorize(permission)for role and capability checks - Queries are tenant-scoped with a mandatory organization filter, closing cross-tenant IDOR (a valid ID from the wrong tenant)
- Operator actions re-query the database on every call and ignore the token, so platform access is revoked instantly
The session token is treated as a cache, never as the authority for anything destructive. A suspended pharmacy is locked out on its next server render, not on its next login.
Append-only inventory ledger
The design decision I am most confident in: a pharmacy stock item has no quantity field at all. On-hand quantity is the sum of an immutable movement ledger (initial, add, remove, sold, adjustment), and every movement is an independent insert.
Two staff members adjusting the same item at once cannot clobber each other, because nobody reads and then writes a shared counter: they each append. Corrections are new rows, not edits, so the stock history is a complete, tamper-evident audit trail and "why does this say 6?" always has an answer. Stock status (in stock, low, critical, unavailable) is derived at read time from the sum against per-item thresholds, so it can never drift from the quantity it describes. Quantity costs an aggregation on read; compound indexes keep that to a single indexed pass, and the correctness guarantee is worth far more than it costs.
Checkout that cannot oversell
A customer's cart can span several pharmacies. At checkout it is split into one order per pharmacy, and the whole thing commits inside a single MongoDB transaction:
- Stock is pre-checked against the ledger and rejected with a customer-safe message ("Paracetamol only has 3 left in stock")
- The cart is split by pharmacy, with subtotal, tax and total computed per order
- A transaction opens and the ledger aggregation is re-run inside it, closing the race where another customer drains the stock between the pre-check and the write
- Every order and every "sold" movement is inserted atomically: all-or-nothing across all pharmacies
Line items and customer details are snapshotted onto the order, so a later price or profile edit never rewrites order history. Bulk stock adjustments follow the same discipline and also net deltas across the batch before validating, so a batch of −3 and −2 against a balance of 4 is rejected as a whole rather than half-applied.
Timezone-safe appointment scheduling
Doctor availability is a weekly schedule in the organization's timezone; appointments are stored as UTC instants. Booking converts the chosen date and time to UTC through a DST-safe resolver, derives the day of week in the clinic's timezone, checks it against the doctor's window, then runs interval-overlap detection against existing appointments. Patients see generated slots marked available or booked.
The appointment lifecycle (pending, waiting, checked in, ready, completed or cancelled) follows how a clinic actually runs: a patient rescheduling an approved appointment drops it back to pending for re-approval, while a staff reschedule keeps it approved. "Delayed" is derived, never stored, so it cannot go stale.
Public pharmacy storefront
A public, searchable catalog across every approved pharmacy, filterable by name, price range, category and dosage. The cart is persisted locally, so a guest's cart survives the sign-in round-trip.
Doctors prescribe from the real pharmacy catalog: the medicine picker is fed by live approved inventory, de-duplicated by name and dosage so a drug stocked by five pharmacies appears once. Prescribing deliberately creates no order and no lock-in. The doctor prescribes a medicine, not a vendor.
SuperAdmin operator panel
The platform's control plane: a pharmacy approval queue, per-product moderation (mandatory rejection reasons, bulk review and a per-pharmacy auto-approve toggle), global product categories, organization suspension, ban and reactivation, contact-query replies and platform settings.
Every mutation writes an append-only audit log with actor, action, target and before/after state. Audit writes are best-effort by design, so a logging failure can never break the operation it records. Products are gated by an approval status that controls public visibility, so nothing reaches patients without explicit review or a deliberate auto-approve decision.
Dashboards, email and onboarding
Role-scoped dashboards built on Recharts: hospital admins get revenue trends, patient demographics and top-performing staff; doctors and nurses get the same shell scoped to their own caseload; pharmacies get revenue, sales by category and top sellers. Around that sit transactional email on Resend (verification, password reset, team invitations, order confirmed, order ready for pickup, organization status changes), Cloudinary image uploads, invitation-based team onboarding and organization ownership transfer.
Outcomes
- Four product surfaces on one platform. Hospital, patient, pharmacy and platform operator experiences share one codebase, one auth system and one medicine catalog.
- Delivered at real scale. 71 pages and 27 data models, delivered through 269 commits and 68 merged pull requests between April and June 2026.
- Tenant isolation enforced at five independent layers, so a single missed check can no longer expose another organization's patients.
- Overselling and lost stock updates are impossible by construction. The ledger removes the update race, and the in-transaction re-read removes the checkout race. Nothing relies on retry logic or luck.
- An operator who never needs a developer. The SuperAdmin panel with its full audit trail lets the platform onboard, moderate and police pharmacies without anyone touching the database.
- An abstraction drawn in the right place. The tenancy model absorbed an entire second business domain, pharmacy e-commerce, added after the clinical product, with no changes to the identity or permission core.
- Ready for what comes next. Payments, delivery and subscriptions are already modelled in the schema, so each can be switched on without reshaping live data.