// 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
}| Permission | Reception | Phlebotomist | Lab Tech | Doctor | Branch Mgr | Admin | Owner |
|---|---|---|---|---|---|---|---|
| Create patient | Y | Y | N | N | Y | Y | Y |
| View patient (own branch) | Y | Y | Y | Y | Y | Y | Y |
| View patient (all branches) | N | N | N | N | N | Y | Y |
| View test results | Summary only | N | Y | Y | Summary only | Y | Y |
| Finalise report | N | N | N | Y | N | N | N |
| Process refund | N | N | N | N | Up to ₹5,000 | Up to ₹50,000 | Any |
| Export patient list | N | N | N | N | Own branch only | Y | Y |
| View audit log | N | N | N | N | Own branch only | Y | Y |
- 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
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 (₹85/month for our volume).
## 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).
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)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](https://www.reddit.com/r/india/comments/labtech_pain) 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?
For a 10–20 branch chain, expect ₹12–25 lakh for Phase 1 (3 months, core booking + reporting + RBAC + audit). Plus ₹30,000–₹60,000/month in 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](https://www.meity.gov.in/data-protection-framework) 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 ₹35,000/month 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
