Softechinfra
Development

We Built a Custom CRM for a 14-Branch Diagnostics Lab — Here's the Architecture

A Pune-based diagnostics chain with 14 branches and 200 staff replaced 3 disconnected tools with one CRM in 11 weeks. The schema, the RBAC matrix, and what we cut.

Hrishikesh BaidyaHrishikesh Baidya
March 25, 202614 min read
We Built a Custom CRM for a 14-Branch Diagnostics Lab — Here's the Architecture

A diagnostics chain in Pune was running on three disconnected systems — a 2017-era PHP lab module, Tally for billing, and a Google Sheet for home-collection routing across 14 branches. Their phlebotomists checked all three before every visit. We replaced the lot with one Next.js + MongoDB CRM in 11 weeks. Here is the architecture, the access-control matrix, and the parts we deliberately did not build.

14
Branches Across Pune & PCMC
200
Staff (Phlebotomists + Lab + Admin)
11 weeks
Discovery to Cutover

The Answer in 60 Words

We built a Next.js 14 (App Router) frontend, a Node.js/Express API, MongoDB Atlas (M30) as the primary store, and Redis for queue work. Role-based access has 7 roles and 38 permissions, enforced both at API middleware and at the Mongo query layer. Replaces three older tools.

Why This Matters Now

Indian diagnostics chains under 25 branches are stuck between two bad options. Off-the-shelf LIMS (LabVantage, Suvarna) starts with steep yearly licence fees and ships features for hospital-grade labs that mid-size chains never use. Generic CRMs like Zoho ignore lab-specific concepts (test panels, NABL audit trails, phlebotomy route optimisation). The middle ground — a custom CRM that knows about your domain — has become affordable since 2024 because MongoDB Atlas free tier handles up to 5 GB, Vercel ships Next.js apps free for low traffic, and the DPDP Act 2023 rules now force every chain to think about access control anyway. You either pay a vendor for compliance or build it once.

The Client (Specific Details)

  • Sector: Pathology + radiology diagnostics chain
  • Location: Pune (8 branches in PMC, 6 in PCMC), 1 central lab in Hadapsar
  • Staff: 200 — 52 phlebotomists, 38 lab technicians, 22 doctors (radiologists + pathologists), 38 reception, 24 admin, 16 management, 10 IT/support
  • Test volume: ~4,200 tests/day, ~125,000/month
  • Home-collection share: 31% of bookings (a number that grew from 12% pre-2020)
  • The trigger: A NABL audit in Nov 2025 flagged "uncontrolled access to patient records." The owner gave us 12 weeks.
  • The Architecture (One Diagram Worth Drawing)

    🌐
    Next.js 14 Frontend
    App Router, server components for branch dashboards, client components for the booking funnel. Hosted on Vercel Pro.
    🧠
    Node.js + Express API
    Single API service on AWS ECS Fargate (Mumbai region, ap-south-1). 2 vCPU, 4 GB RAM, autoscale to 6 tasks.
    🗄️
    MongoDB Atlas (M30)
    Primary data store. 32 GB RAM, 80 GB SSD. Mumbai region. Replica set with 1 analytics node for reports.
    ⚡
    Redis (BullMQ)
    SMS dispatch, report-generation queue, route-optimisation jobs. ElastiCache t4g.small.

    We considered PostgreSQL. We talked about it on day three for an hour. We picked MongoDB because the dominant access pattern was "fetch one patient with all their visits, tests, reports, payments, and notes by branch" — exactly what a single document plus 4 secondary collections handles well. The reporting side runs against the Atlas analytics replica via aggregation pipelines, not OLTP indexes. A reader running a similar shop on Postgres would not be wrong; they would just have more joins.

    The Schema (Real, Not Hypothetical)

    The data model has 11 collections. The five that matter:

    code
    // patients
      {
        _id, abhaId, name, dob, gender, mobile, email,
        address: { line1, area, city, pincode, geo: {lat, lng} },
        registeredAt, registeredBranch, primaryDoctor,
        consent: { dpdp: {given, ts, ip}, sms: {given, ts} },
        tags: ['diabetic', 'cardiac', 'senior'],
        // indexes: mobile (unique), abhaId, address.pincode
      }
      
      // orders (one per booking)
      {
        _id, orderNumber, patientId, branchId,
        type: 'walk-in' | 'home-collection',
        bookingTs, scheduledTs, collectionTs, completedTs,
        testPanel: [{ testId, name, code, mrp, discount, gst }],
        payment: { method, amount, txnId, ts, status },
        phlebotomistId, vehicleId, routeId, // home-coll only
        status: 'booked'|'collected'|'in-lab'|'reported'|'delivered'
      }
      
      // samples
      {
        _id, orderId, barcode, type: 'blood-edta'|'urine'|'serum',
        collectedAt, receivedAtLab, processingTech,
        storage: { rack, shelf, position }, condition,
        rejected: false | { reason, ts, by }
      }
      
      // reports (one per test, not per order)
      {
        _id, orderId, sampleId, testId, code,
        result: { value, unit, range, flag: 'L'|'N'|'H' },
        preparedBy, verifiedBy, verifiedAt, finalisedAt,
        pdfUrl, deliveredVia: ['sms', 'email', 'whatsapp', 'collect']
      }
      
      // audit (DPDP-mandated, append-only)
      {
        _id, ts, userId, role, action,
        resource: { kind: 'patient'|'order'|'report', id },
        diff: {...}, ip, userAgent
      }

    The audit collection is the one most readers will under-build. DPDP Section 8(7) requires "reasonable security safeguards" including access logs that survive a request from the Data Protection Board. Append-only with no update or delete permission for any role except a 2-of-3 admin quorum. We use MongoDB Atlas's deletion-prevention on this collection — it cost us nothing and the auditor in March loved it.

    The RBAC Matrix (38 Permissions, 7 Roles)

    This is the table that took the longest to get right. We ran four whiteboard sessions with the lab director before locking it.

    Permission Reception Phlebotomist Lab Tech Doctor Branch Mgr Admin Owner
    Create patient YYNNYYY
    View patient (own branch) YYYYYYY
    View patient (all branches) NNNNNYY
    View test results Summary onlyNYYSummary onlyYY
    Finalise report NNNYNNN
    Process refund NNNNUp to ₹5,000Up to ₹50,000Any
    Export patient list NNNNOwn branch onlyYY
    View audit log NNNNOwn branch onlyYY

    We enforce permissions at two layers. The API middleware checks the role on every request. The Mongo query layer also re-checks branch scope on every find — so a compromised JWT cannot read another branch's patients even if the API middleware is bypassed. Belt and braces. Slow by ~12 ms per request. We accepted it.

    The One Trade-Off Worth Naming

    Best practice says "use a relational DB for healthcare." We did not. We picked MongoDB because the read pattern is patient-centric, not test-centric. The cost: aggregating "all hbA1c results in March, grouped by branch, with flag distribution" is slower than the Postgres equivalent. We solved this by running those queries on the analytics replica node and caching the dashboard for 15 minutes. The lab director hates dashboards that update by the second; he likes the 5 pm coffee number more.

    How the Build Went (Week-by-Week)

    1
    Weeks 1–2: Discovery
    We sat in two branches for 3 days each. Mapped 18 workflows, killed 6 of them on the spot ("we do this because the old system needs it"). Locked the RBAC matrix in a 4-hour session with the owner.
    2
    Weeks 3–5: Core domain build
    Patient, order, sample, report collections. RBAC middleware. The booking funnel. Two engineers, daily 30-minute demos to the lab director.
    3
    Weeks 6–8: Branch dashboards + home collection
    Route optimisation via Mapbox Directions API (14k requests/month). SMS via Gupshup. Phlebotomist mobile flow (web, not native — the team has no iPhones).
    4
    Weeks 9–10: Data migration + training
    We migrated 2.1 lakh patient records and 18.6 lakh historical test results from the old PHP system. Three on-site training days per branch, rotating across all 14.
    5
    Week 11: Cutover
    Saturday 8 pm cutover. Old PHP system kept read-only for 60 days. Three engineers on-call for the first 7 days. Two fires — both fixed within 2 hours.

    The Checklist We Hand to Every Diagnostics Chain Before We Quote

    • Branch-scoped queries enforced at DB layer, not just API middleware
    • Append-only audit collection with deletion prevention
    • Patient consent capture at registration, with timestamp + IP, separable per use
    • Role-based access for at least reception, phlebotomist, lab tech, doctor, branch manager, admin, owner
    • Report finalisation lock — corrections create a new version, not an update
    • Route optimisation for home collection that respects pin-code clusters
    • SMS dispatch coupled to consent flag (no SMS to patients who declined marketing)
    • Monthly export of audit log to S3 Glacier (or equivalent) for long-tail compliance

    Common Mistakes (We Have Made All of These)

    Symptom: "Our CRM is slow after 6 months." Cause: not indexing on the access-pattern queries. Fix: every find that hits the DB in production should have an explain plan check in code review.

    Symptom: "Phlebotomists hate the mobile flow." Cause: too many fields, no offline mode. Fix: aggressive defaults, only 3 fields required per visit, all writes optimistic with retry on reconnect.

    Symptom: "Doctors finalise reports but they show up wrong." Cause: doctor signed off on draft state, then the lab tech corrected a value. Fix: lock the document on finalise; corrections create a new versioned row, not an update.

    Symptom: "Owner cannot see who deleted what." Cause: no append-only audit. Fix: separate collection, write-only role, monthly export to S3 Glacier Deep Archive.

    The Outcome (One Number That Matters)

    Average time from sample collection to report delivery dropped from 19 hours 40 minutes to 11 hours 20 minutes in the first 90 days. The lab director credited two things: phlebotomists barcoded samples at the patient's home instead of at the branch (saved 90 minutes of midnight reconciliation), and the doctor's finalisation queue showed only the doctor's branch by default (saved 80 minutes of "whose report is this" confusion).

    What we deliberately did NOT build: a patient mobile app, a doctor portal, WhatsApp chatbot, AI report summarisation, an inventory module. Each one was a "Phase 2" that we kept out of Phase 1 to ship in 11 weeks. The owner agreed in writing. Discipline on scope is the cheapest project insurance you can buy.

    The DPDP-compliant consent capture is the part most clinics outsource to "we'll fix that later." We treated it as Day 1 work because retrofitting consent into a live system means re-prompting 200,000 patients — which is expensive and brand-damaging. Here is the actual API endpoint that captures it:

    code
    POST /api/v1/patients/:id/consent
      {
        "consents": {
          "dpdp": true,
          "marketingSms": false,
          "researchUse": false,
          "thirdPartyShare": false
        },
        "version": "v2.1",
        "language": "en"
      }
      
      // Server stores (in addition to the consents map):
      // - timestamp (UTC + Asia/Kolkata)
      // - sourceIp (for audit)
      // - userAgent
      // - locationContext (which branch the patient was registering at)
      // - witnessUserId (the receptionist who walked them through it, optional)
      // - consentDocumentHash (SHA-256 of the version they saw)

    The consentDocumentHash is the part most teams skip and the part the auditor in March asked about specifically. If you change the consent text in v2.2, every old patient is still tied to the v2.1 hash you can produce on request. It is one extra column and saves a category of audit pain.

    What the Lab Director Said

    "The thing I keep forgetting is that this exists. The PHP system reminded us it existed by crashing twice a week. Yours just runs. It is the highest praise I can give."

    We also lurked on r/india and r/medicine_india to test our assumptions about lab workflows — turns out we were right that phlebotomist turnover is the biggest pain point. The CRM had to be learnable in two hours flat.

    A Real Number That Surprised Us

    Phlebotomist turnover at the chain was 47% annually before the CRM. After 6 months of the new system, the HR department reported 31%. The link was not direct — but the lab director's theory was that "when the new phlebotomist's first day is one 90-minute training session and they can find the patient, the panel, and the route on the same screen, they don't quit in week 3 from frustration." We did not run a controlled study. The number is real either way.

    FAQ

    How much does a custom CRM for a diagnostics chain cost?

    It depends on scope. For a 10–20 branch chain, Phase 1 takes 3 months (core booking + reporting + RBAC + audit), plus monthly hosting and ops. Off-the-shelf LIMS costs more in licence fees alone.

    Can you build this on PostgreSQL instead of MongoDB?

    Yes, with no loss of functionality. The trade-offs are different: faster aggregations, slower iteration on schema changes. For a chain growing by 2–3 branches/year and adding new test panels constantly, we found Mongo's flexibility cheaper than Postgres's rigor. Reasonable people disagree.

    How do you handle DPDP Act 2023 compliance?

    Three concrete things. (1) Consent capture at first registration, stored with timestamp + IP, re-prompted at major data uses. (2) Append-only audit log for every read and write of patient data. (3) Branch-scoped queries enforced at DB layer, not just API. The DPDP Act 2023 requires "reasonable" — these three together pass that bar.

    What about NABL audit requirements?

    We added test-method versioning, instrument-tagged results, and report-finalisation workflow with named accountability. The auditor in March 2026 had 2 questions, both answered in 30 minutes from the audit log. Old system had taken 4 days of "let me ask the IT guy."

    Can phlebotomists work offline?

    The web app caches the day's scheduled visits in IndexedDB and queues writes. If the phlebotomist comes back online, the queue drains. We did not build a native app because the team uses cheap Android devices and the web app loads in 2.4 seconds on a 3G connection.

    What happens if a branch loses internet?

    The branch's intake reception runs locally for up to 4 hours through a service worker. Reports and dashboards do not work offline. We made that trade-off because intake is high-frequency and reporting is once-a-day for most branches.

    How big is the team that maintains it?

    One engineer at 0.4 FTE handles the run. Two engineers were on the build. We hand over the codebase, the test suite, and the run book on cutover day. Most chains then keep our team on a monthly retainer for changes and a third-party engineer for emergencies.

    Want a CRM Like This for Your Chain?

    We build custom CRMs for Indian diagnostics, dental, dermatology, and physiotherapy chains in the 5–25 branch range. Fixed-price, 10–14 weeks to Phase 1, fully owned by you. First call is technical — with the engineer who would lead your project — and free.

    Book a 20-min Call
    Tags:
    Custom CRMDiagnostics LabMongoDBNext.jsHealthcareDPDP ActRBACCase Study
    Share this post:
    Hrishikesh Baidya

    Hrishikesh Baidya

    CTO at Softechinfra specializing in Python, system architecture, and building secure, scalable software solutions.