The Platform Behind the App: How OpesCare Runs Every Desk
Jude Nshome on OpesCare's web platform: one patient index, a FHIR ingest layer and pharmacy, lab, blood and front-desk workflows on one shared record.
The web platform is not an admin panel bolted onto an app. It is the other side of the same record: one patient index, one way in for partner data, and the everyday workflows of pharmacies, laboratories and blood banks, rebuilt so that they share a record instead of copying one.
The patient-facing app, described in The Record Is the Product, is the visible half of OpesCare. The other half is the web platform at opescare.com, which serves institutions: the independent pharmacy, the district clinic, the diagnostic laboratory, the imaging service, the blood bank — and the hospital that combines several of them.
This essay walks inside it: the Master Patient Index that settles who a patient is, once; the interoperability layer that lets partner systems send records in; and the desk workflows — registration, triage, queue, prescribing, dispensing, sample-to-release and blood requests — built around the same record the patient holds on their phone. It is also careful about what the platform is not. OpesCare is a discovery, identity and coordination product. It is not a hospital information system, and it does not pretend to be one.
- A client calls opescare.com The website, the app, partner systems and the Bridge Agent reach one of three entry points: web routes, the REST API or the FHIR R4 endpoint.
- Authentication Web routes use a session; the API checks a signed JWT and its scopes.
- Consent gate A clinician read passes the clinical relationship gate before any service answers.
- FHIR ingest guard Partner writes pass the three ingest gates: facility from the token, an existing Health ID, an active grant.
- Stored and logged Services read and write PostgreSQL, and every read of a record is written to the access log.
- Refused A failed check answers 401 or 403 and nothing is read or written.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Clients | route:docs.index route:docs.widget route:docs.bridge |
| Web routes | route:login route:public.network.medicine-finder |
| REST API | uri:POST api/v1/connect/auth/token uri:GET api/mobile/consents |
| FHIR R4 ingest | uri:POST api/fhir/R4 uri:GET api/fhir/R4/metadata |
| Authentication | route:login.submit method:App\Http\Controllers\Api\V1\Connect\AuthController::introspect |
| Consent gate | method:App\Support\ClinicalRelationshipGate::evaluate config:staff_capabilities.capabilities.clinical_chart |
| Ingest guard | method:App\Services\Fhir\FhirIngestGuard::requireConsent |
| Refused | route:docs.errors |
| OpesCare services | class:App\Modules\PatientIdentity\Services\PatientIdentityService class:App\Services\WebhookService route:public.care-map |
| PostgreSQL | config:database.connections.pgsql |
| Access and audit log | class:App\Models\AccessLog uri:GET api/mobile/access-logs |
| Response | route:docs.api |
1. What the platform is
One Laravel application on PostgreSQL serves the public website, the institutional consoles, and one /api surface shared by the mobile app and by partner integrations. Each institution is onboarded, worked through a go-live checklist and readiness score by a platform administrator, and from then on takes part in the shared patient index while running its own desks.
- Listing claimed The owner claims the facility's listing in the public care map; an administrator reviews the claim.
- Listing verified An approved claim lets the listing be joined to the facility. Approving a claim does not start onboarding.
- Onboarding begins Onboarding is a separate path, worked through a checklist.
- Readiness scored The facility's readiness for go-live is scored.
- Go-live decision An administrator approves go-live.
- Team and desks Staff join by invitation or approved join request, and the pharmacy, lab and queue desks open.
- Not ready yet A facility that is not ready goes back to the checklist.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Facility owner | route:public.care-map |
| Listing claimed | route:public.care-map.claim route:public.care-map.claim.store class:App\Http\Controllers\CareMap\FacilityClaimController |
| Claim reviewed | route:admin.care-map.review route:admin.care-map.review.claims.approve |
| Listing verified | route:admin.care-map.review.listings.link |
| Onboarding checklist | route:portals.admin.onboarding.show route:portals.admin.onboarding.mark method:App\Http\Controllers\MedicalId\OnboardingPortalController::markItem |
| Readiness scored | route:portals.admin.facilities.readiness route:portals.admin.facilities.readiness.score class:App\Http\Controllers\MedicalId\FacilityGoLiveReadinessController |
| Claim rejected | route:admin.care-map.review.claims.reject |
| Go-live decision | route:portals.admin.onboarding.approve method:App\Http\Controllers\MedicalId\OnboardingPortalController::approve |
| Team joins | route:portals.facility.team.join.approve route:professional.join.store |
| Desks live | route:portals.pharmacy.stock route:portals.lab.index route:portals.queue.index |
| Who | What they use on the web today |
|---|---|
| Facility administration | Team invitations and roles, facility settings, a ward and bed register for occupancy counts, invoices issued |
| Front office | Triage desk with reassessment; the day's queue — check-in, call, start, complete, no-show |
| Clinician | Prescribing with an interaction check, lab ordering, referrals, immunisation records, a consent-scoped chart |
| Pharmacy | Prescriptions and dispensing, medicine orders from patients, stock, batches, receipts, adjustments, reorder notes, prices, stock import, a controlled-medicines register, reports |
| Laboratory | Test requests, collection, results, validation, release, a manager dashboard with critical-value call-backs |
| Imaging | Imaging orders scheduled, performed and reported, and a public imaging-exam finder |
| Blood bank | Blood inventory by product and group, and a blood-request desk |
| Platform administration | Master Patient Index desk, facility onboarding and readiness, partner credentials, integration certifications, DHIS2 aggregate reporting, breach register, legal documents |
| Developers | Applications and credentials, webhook subscriptions, delivery history and replay, documentation |
None of these is a mock-up. Each has a registered route, a controller, a page and a link in the navigation for the role that uses it. The common thread is that they are not separate products: one patient, one consent model, one audit trail.
2. The Master Patient Index: who this person is
A national network fails quietly if the same person exists twice. The Master Patient Index (MPI) is therefore a governed process, not a search box.
- Exact identifiers at the desk. Front-desk registration and booking work on the exact Health ID. A desk cannot browse the population by typing part of a name; if a registration looks like an existing identity, it is refused rather than duplicated. Every path that creates a patient goes through the same duplicate check.
- Detection runs on a schedule. A nightly scan looks at recent registrations and a weekly scan looks wider. Both raise candidate pairs; neither merges anything.
- A person decides. At the MPI desk an administrator reviews each pair with the evidence side by side and either merges or rejects it. Staff can also raise a pair by hand when they spot one.
- Merges are reversible. A merge records which record survived and keeps the other identifier as an alias, so an old Health ID or QR code still resolves to the right person. A merge can be undone, most recent first, from the same desk.
- The card maps to one row. QR tokens are short-lived and the Health ID resolves through the alias table, so the card in the patient's hand points to exactly one entry in the index.
- Run duplicate scan Scans run nightly and weekly on the schedule.
- Score candidate pairs Records are compared within blocks so the scan stays fast on a national register.
- Queue pairs for review Likely duplicates are raised as candidates. Nothing merges automatically.
- Compare side by side An administrator reviews both records. A pair can also be raised by hand.
- Merge: survivor + alias One Health ID survives; the other becomes an alias that still resolves.
- Lookups follow the merge Searches, scans and links on the old ID land on the survivor.
- Or reject the pair A rejected pair is dismissed and both records stay as they are.
- Unmerge if needed Merges are reversible, last in first out, from the Merged tab.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Scheduler | command:mpi:detect class:App\Console\Commands\DetectDuplicatePatients |
| Score candidate pairs | method:App\Modules\MasterPatientIndex\Services\MasterPatientIndexService::detectDuplicates method:App\Modules\MasterPatientIndex\Services\MasterPatientIndexService::scoreMatch |
| Queue pairs for review | class:App\Models\MpiCandidate route:portals.admin.mpi.index route:portals.admin.mpi.candidates.raise |
| Compare side by side | route:portals.admin.mpi.candidates.show |
| One Health ID | class:App\Models\PatientMergeAlias |
| Lookups follow the merge | method:App\Models\Patient::findExistingIdentity class:App\Models\PatientMerge |
| Merge: survivor + alias | route:portals.admin.mpi.candidates.merge method:App\Modules\MasterPatientIndex\Services\PatientMergeService::merge |
| Or reject the pair | route:portals.admin.mpi.candidates.reject method:App\Modules\MasterPatientIndex\Services\MasterPatientIndexService::rejectMatch |
| Records separated | method:App\Modules\MasterPatientIndex\Services\PatientMergeService::unmerge |
| Unmerge if needed | route:portals.admin.mpi.merges.unmerge |
This carries the "one person, many facilities" argument. The questions a national index must answer — who is this? and, a year later, are they in here twice? — are answered by a process with human review and an audit trail, not by whoever happened to type the name.
3. The interoperability layer
OpesCare treats interoperability as a discipline that starts with what comes in.
FHIR in, and the patient's own export out
- Partner ingest. Approved partners send FHIR R4 resources — DiagnosticReport, Observation, Immunization, and transaction bundles — to a scoped endpoint with a published capability statement. Each partner holds bearer credentials limited to the scopes an administrator granted. A guard checks that the partner may write about that patient and that the resource can be mapped; when it cannot, the ingest refuses loudly instead of storing something in the wrong place.
- The patient's export. From the mobile app a patient can export their own record as a FHIR bundle, and import one.
- What is absent. There is no outbound FHIR server for third parties to query, and no HL7 v2 interface. That is a deliberate choice: records leave OpesCare when the patient takes them, not when a system asks.
- POST /api/fhir/R4 Bundle A Bundle of type transaction with a bearer token. Single resources post to /Observation, /DiagnosticReport or /Immunization.
- Verify JWT and scope The token signature and scopes are checked; each entry needs the scope for its resource type.
- Gate 1: facility from token The writing facility is taken from the verified token only, never from the payload.
- Gate 2: existing Health ID The patient must resolve exactly to an existing Health ID. A partner never creates one.
- Gate 3: active consent grant The patient must hold an active grant to that facility. No grant, no write.
- Store every entry, or none All entries are written in one database transaction; any refusal rolls back the lot.
- 200 transaction-response Each entry reports 201 Created. A single-resource POST returns 201.
- Refusal: OperationOutcome Any failed gate returns a FHIR OperationOutcome and nothing is stored.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Partner system | uri:POST api/fhir/R4 uri:GET api/fhir/R4/metadata |
| Verify JWT and scope | method:App\Services\Fhir\FhirIngestGuard::scopes method:App\Services\Fhir\FhirIngestGuard::requireScope |
| Gate 1: facility from token | method:App\Services\Fhir\FhirIngestGuard::facilityId |
| Gate 2: existing Health ID | method:App\Services\Fhir\FhirIngestGuard::resolveSubject |
| Refusal: OperationOutcome | method:App\Services\Fhir\FhirIngestGuard::recordRefusal |
| 200 transaction- response | method:App\Services\Fhir\FhirIngestGuard::recordAccepted uri:POST api/fhir/R4/Observation |
| Store every entry, or none | method:App\Http\Controllers\Api\Fhir\FhirIngestController::transaction class:App\Services\Fhir\FhirIngestService |
| Gate 3: active consent grant | method:App\Services\Fhir\FhirIngestGuard::requireConsent class:App\Models\ConsentGrant |
Vocabularies as data, not literals
Diagnoses, tests, medicines and units are coded against published vocabularies imported as data: ICD-10 from the World Health Organization, with French titles; LOINC for laboratory observations; ATC for medicines; and UCUM for units of measure, so that milligrams and micromoles per litre are never confused at a handover. A code dictionary sits on top of them, so that a code arriving through ingest and a code chosen at a desk mean the same thing.
A developer surface with guard rails
The developer portal lets an organisation register applications and obtain credentials, subscribe to webhooks, see each delivery and replay a failed one. A partner can also push pharmacy and blood stock levels through the Connect API, which feeds the finder only when the source is on the availability allow-list. An integration certification checklist is kept by platform administrators. Documentation lives at opescare.com/docs. A laboratory system vendor can integrate against a permissioned, audited surface rather than an email thread.
National reporting, aggregate only
OpesCare can report its own monthly aggregate counts — never patient-level data — to the national DHIS2 instance run by MINSANTE. Facilities are mapped to organisation units and counts to data elements; a draft is generated on the first of each month, and a platform administrator reviews and approves it before anything is sent. Today the drafts are generated, but nothing is pushed: that waits on MINSANTE granting access to the national instance. This is one-way reporting, not a health-data exchange.
4. The front office and the clinic
For a clinic, the platform covers the parts of the day that touch OpesCare patients:
- Triage records vital signs against adult reference bands and supports reassessment. It re-orders the queue by urgency; it does not shorten anyone's wait, and it never blocks care.
- The queue takes a patient from check-in to call, start and completion, or marks a no-show, and is sorted on the server so every screen sees the same order.
- Prescribing runs an interaction check before a prescription is issued, and the printed prescription carries a verification code.
- Referrals are sent, accepted, completed or declined between clinicians; patients can request one from the app.
- Immunisations are recorded against the patient and can be voided, never silently edited.
- Invoices can be issued and paid by mobile money — unavailable until the merchant accounts are connected — and care is never gated on payment.
What the clinic side is not: full clinical documentation, admissions, discharges and transfers, bed-management workflows, insurance claims and pre-authorisation. A ward and bed register exists so a facility can report occupancy; it is a register, not an admissions system.
5. The pharmacy: not a stock list — a workflow
An independent pharmacy gets a complete operational console, built around dispensing.
- Prescriptions arrive in one list — from clinicians on OpesCare and from patients who share them — and move through accepted → ready → dispensed. A prescription written at one facility can be filled at another; the patient carries it.
- Patient orders — medicine reservations placed from the app — follow their own path: accepted, ready, then collected or dispatched and delivered, or cancelled.
- Stock is what the pharmacy actually holds: stock levels and batches, receipts of incoming deliveries, adjustments with a reason, reorder notes, a price list with history, and spreadsheet import with a preview, column mapping and an error report before anything is committed.
- Controlled medicines have their own register.
- Reports export dispensing and stock figures as CSV or PDF.
- Prescription issued The clinician writes the prescription; an interaction check runs before it is saved.
- Shared with a pharmacy The patient may share the prescription from the app so it appears in that pharmacy's list.
- Looked up at the counter Without sharing, the pharmacist finds it by the patient's Health ID. It can be written at one facility and filled at another.
- Accepted and prepared The pharmacist accepts the prescription and marks it ready.
- Dispensed Dispensing records what was given, decrements stock and prints the medicine labels.
- Medicine reserved A patient reserves a medicine in the app; the pharmacy accepts the order and prepares it.
- Collected The patient collects the order at the counter.
- Delivered Or the pharmacy dispatches it and marks it delivered.
- Cancelled An order can be cancelled by the patient or the pharmacy while it is still open.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Clinician | config:staff_capabilities.capabilities.prescribe |
| Prescription issued | route:portals.prescriber.store route:portals.prescriber.interaction_check method:App\Http\Controllers\MedicalId\PrescriberController::store |
| Patient | uri:GET api/mobile/prescriptions |
| Medicine reserved | uri:POST api/mobile/pharmacy/reservations method:App\Http\Controllers\Api\Mobile\MobilePharmacyController::reserve |
| Pharmacy list | route:portals.pharmacy.prescriptions route:portals.pharmacy.prescriptions.lookup uri:GET api/mobile/pharmacy-desk/prescriptions/lookup |
| Shared with a pharmacy | uri:POST api/mobile/prescriptions/{id}/share method:App\Http\Controllers\Api\Mobile\MobilePrescriptionController::share |
| Cancelled | route:portals.pharmacy.orders.cancel method:App\Http\Controllers\Api\Mobile\MobilePharmacyController::cancelReservation |
| Order accepted | route:portals.pharmacy.orders.accept method:App\Http\Controllers\Api\Mobile\Pharmacy\PharmacyReservationController::accept |
| Accepted | method:App\Http\Controllers\Api\Mobile\Pharmacy\PharmacyDeskController::accept |
| Ready | method:App\Http\Controllers\Api\Mobile\Pharmacy\PharmacyDeskController::ready |
| Dispatched | route:portals.pharmacy.orders.dispatch |
| Order ready | route:portals.pharmacy.orders.ready |
| Dispensed | route:portals.pharmacy.dispense method:App\Http\Controllers\Api\Mobile\Pharmacy\PharmacyDeskController::dispense class:App\Services\Clinical\PrescriptionService route:documents.new.labels |
| Delivered | route:portals.pharmacy.orders.delivered |
| Collected | route:portals.pharmacy.orders.collected |
Two limits matter. Stock reported by a pharmacy becomes finder availability only when its source is on the allow-list, so an empty finder is the honest answer rather than a demo filled with invented stock. And procurement — purchase orders, supplier contracts — is not part of OpesCare; a simple supplier list exists so receipts can say where stock came from.
6. The laboratory: sample to release
Laboratories run a release workflow, not an inbox.
- Requests from clinicians or patients are approved or declined by the laboratory.
- Orders move through collection, result entry, validation and release. A released result can be corrected, but the correction is recorded, never overwritten.
- Release notifies both sides at once. The patient receives the result in their app and the clinician who ordered the test is told in the same step. A critical value is flagged as high priority, and the laboratory records its telephone call-back to the clinician.
- A manager dashboard shows turnaround, pending work and critical alerts.
- Clinician orders a test The clinician orders the test from the patient's chart.
- Or the patient asks A patient can request a test in the app; the lab manager approves it into an order or declines it.
- Sample collected The lab records the sample collection.
- Results entered Results are entered against the order.
- Validated Results are validated before they can be released.
- Released Release runs as one locked step.
- Patient notified The patient gets a bell notification and a push, marked high priority when a value is critical.
- Clinician notified The ordering clinician is notified in the same step.
- Guardians told once Guardians linked to the patient are told once.
- Critical call-back For a critical value, the lab records the phone call-back it made.
- Correction after release A later correction is recorded beside the original value, never written over it.
- Refused without results Release is refused when there are no results to release.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Patient | uri:POST api/mobile/labs/requests |
| Clinician | config:staff_capabilities.capabilities.clinical_chart |
| Test ordered | uri:POST api/mobile/doctor/patients/{patient}/lab-orders method:App\Http\Controllers\Api\Mobile\MobileDoctorLabOrderController::store |
| Sample collected | route:portals.lab.collect method:App\Http\Controllers\MedicalId\LabDeskWebController::collect |
| Test requested | uri:POST api/mobile/labs/requests |
| Lab manager decides | route:portals.lab_manager.requests.approve route:portals.lab_manager.requests.decline |
| Release refused | method:App\Services\Lab\LabDeskService::release |
| Results entered | route:portals.lab.results.store |
| Declined | route:portals.lab_manager.requests.decline |
| Correction recorded | route:portals.lab.results.correct |
| Release | route:portals.lab.release method:App\Services\Lab\LabDeskService::release |
| Validated | route:portals.lab.validate |
| Guardians told | method:App\Listeners\NotifyGuardiansOfPatientEvent::handleLabResult |
| Patient notified | method:App\Services\Lab\LabDeskService::notifyReleased method:App\Services\Lab\LabDeskService::bellPatient method:App\Services\Lab\LabDeskService::pushPatient |
| Clinician notified | method:App\Services\Lab\LabDeskService::notifyReleased |
| Call-back recorded | route:portals.lab_manager.critical_callback method:App\Http\Controllers\MedicalId\LabManagerDeskController::criticalCallback |
Imaging follows the same shape for orders: requested, scheduled, started, completed, reported, then finalised and distributed, with an amendment path. OpesCare stores the report and its attachment; it is not an image archive and has no PACS or DICOM viewer.
7. The blood bank: supply meets demand
- Inventory is kept by blood product and group, with adjustments and flags, and can also be synced from a partner system.
- The request desk takes each request from a clinician or a patient through decision, ready and fulfilled, and prints a request slip.
- Donors can register their willingness to donate from the app.
- The patient side closes the loop. The blood finder a patient searches is fed by the same inventory the blood bank maintains, and a request made in the app arrives at the same desk.
- Request from the app A patient asks for blood from the app, giving the group, the number of units and the urgency.
- Request from a facility A clinician or facility desk raises the request on the patient's behalf.
- Checked against inventory The blood bank decides with its own inventory in view.
- Ready, then fulfilled An accepted request is set aside as ready and then fulfilled; the request slip prints as a PDF.
- Declined A declined request ends there and the requester is told.
- Cancelled A patient can cancel their own request.
- The finder reads the inventory The public blood finder shows availability from the same inventory.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Clinician | config:staff_capabilities.capabilities.blood_inventory |
| Request from the desk | route:portals.staff.blood_requests.store method:App\Http\Controllers\MedicalId\BloodRequestDeskController::store |
| Blood bank decides | route:portals.staff.blood_requests.decide uri:POST api/mobile/blood-desk/requests/{id}/decide method:App\Http\Controllers\Api\V1\BloodRequestQueueController::decide |
| Blood inventory | route:portals.staff.inventory.blood route:portals.staff.inventory.blood.adjust uri:POST api/v1/connect/inventory/blood-stock/sync |
| Patient | uri:GET api/mobile/blood/requests |
| Request from the app | uri:POST api/mobile/blood/requests method:App\Http\Controllers\Api\Mobile\MobileBloodController::store |
| Ready | route:portals.staff.blood_requests.ready method:App\Http\Controllers\Api\Mobile\MobileBloodDeskController::ready |
| Shown in the finder | route:public.network.blood-finder uri:GET api/mobile/blood/search |
| Cancelled | uri:POST api/mobile/blood/requests/{id}/cancel |
| Fulfilled | route:portals.staff.blood_requests.fulfil method:App\Http\Controllers\Api\Mobile\MobileBloodDeskController::fulfil route:documents.new.blood_request |
| Declined | route:portals.staff.blood_requests.decide |
8. The governance spine
Everything above runs on the same governance as the app.
- Consent as a protocol. Every clinician read of a patient passes a gate that requires a care relationship — an appointment, a care team, an open episode at that facility — or a consent grant the patient approved.
- Access logs. Reads are logged, and the patient's own access-log view is the same log.
- Break-glass. Emergency access requires a reason, opens a limited profile, alerts the patient and is reviewed.
- Security operations. Administrators keep a breach register in which an incident is opened, contained, notified and closed.
- Legal documents — terms and policies — are versioned, published from the administration console and readable by anyone at a public URL.
- Look up Health ID (scan or type) The clinician scans the patient QR or enters the Health ID.
- Identity only, no chart Without an appointment, care-team link or grant, the record stays closed.
- Request consent: scope + reason The request names what will be read and why.
- Notify patient The request appears in the patient app with a push notification.
- Approve (or deny) The patient decides. A denial ends the flow here.
- Grant: scoped, expires_at set A consent grant is stored with its scope and expiry time.
- Open the chart The clinician reads the record.
- Gate checks basis, scoped read The clinical relationship gate admits only what the grant covers.
- Access logged Who read what, when and on which basis is written to the access log the patient can see.
- Scoped data returned Only the granted part of the record comes back.
- Revoke, or grant expires The patient can revoke at any time; otherwise the grant lapses at its expiry.
- Request denied If the patient denies the request, no grant is created and the chart stays closed.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Clinician | config:staff_capabilities.capabilities.identify_patient route:portals.chart.consent_request |
| Look up Health ID (scan or type) | uri:POST api/mobile/doctor/patients/scan method:App\Http\Controllers\Api\Mobile\MobileProviderScanController::scan |
| Basis for reading? | method:App\Support\ClinicalRelationshipGate::evaluate method:App\Support\ClinicalRelationshipGate::holds |
| Request consent: scope + reason | uri:POST api/mobile/doctor/consent-requests method:App\Http\Controllers\Api\Mobile\MobileConsentRequestController::store config:staff_capabilities.capabilities.request_access |
| Scoped data returned | uri:GET api/mobile/doctor/patients/{patient} method:App\Http\Controllers\Api\Mobile\MobileDoctorChartController::show |
| Access logged | class:App\Models\AccessLog uri:GET api/mobile/access-logs |
| Gate checks basis, scoped read | method:App\Support\ClinicalRelationshipGate::hasActiveConsentGrant method:App\Support\ClinicalRelationshipGate::coversFullRecord |
| Notify patient | uri:GET api/mobile/consent-requests uri:GET api/mobile/consent-requests/pending-count |
| Further reads refused | uri:POST api/mobile/consents/{id}/revoke method:App\Http\Controllers\Api\Mobile\MobileGovernanceController::revokeConsent |
| Grant: scoped, expires_at set | class:App\Models\ConsentGrant method:App\Http\Controllers\Api\Mobile\MobileGovernanceController::approveConsent |
| Approve (or deny) | uri:POST api/mobile/consent-requests/{id}/approve uri:POST api/mobile/consent-requests/{id}/deny |
| Request denied | method:App\Http\Controllers\Api\Mobile\MobileGovernanceController::denyConsent |
9. Under the hood (for engineers)
- Backend: Laravel on PHP 8.3 and PostgreSQL. Background work runs on a queue worker managed by the operating system, with separate queues for imports and webhooks.
- Documents: PDFs — prescriptions, lab requests, invoices, receipts, labels — are rendered on demand on a secure sheet with a verification QR code, so a printed page can be checked against the server.
- API: the mobile app is an API client like any other; the OpenAPI description is generated from the code.
- Audit: patient-record access from every desk is written to the audit trail with the reason and the actor.
- Show Health ID QR The patient opens the Health ID card in Presentation Mode.
- Scan, open /verify/qr/{token} Any camera opens the verification page on opescare.com.
- Check token state The token is looked up and checked for expiry and revocation.
- Verified, expired, revoked or invalid The page shows exactly one of the four states.
- Record successful scan A successful verification is stored as a verification event.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Show Health ID QR | route:portals.patient.health-id-card method:App\Services\Identity\QrTokenService::generateToken |
| Scan, open /verify/qr/{token} | route:verify.qr |
| Check token state | method:App\Http\Controllers\MedicalId\VerifyController::qr method:App\Services\Identity\QrTokenService::verifyToken method:App\Services\Identity\QrTokenService::failureState |
| Record successful scan | class:App\Models\HealthIdVerificationEvent |
| Invalid | method:App\Services\Identity\QrTokenService::failureState |
| Revoked | method:App\Services\Identity\QrTokenService::revokeToken |
| Expired | class:App\Models\HealthIdQrToken |
| Verified | route:verify.qr |
10. Where the honest limits are
- Production, not proof of scale. OpesCare has served opescare.com in production since 19 October 2026. We do not publish adoption figures here.
- One country. OpesCare is built for Cameroon, in English and French. There is no multi-country rollout.
- DHIS2 waits on MINSANTE. Monthly drafts are generated; nothing is sent until access to the national instance is granted.
- Payments wait on merchant accounts. Mobile-money payment is built and unavailable until those accounts are connected.
- Push notifications are not yet in the store build; they wait on store credentials.
- Not a hospital information system. No admissions, no inpatient nursing, no insurance claims, no procurement, no PACS. Those belong elsewhere, and saying so keeps the platform honest.
11. The platform in one paragraph
One patient index — checked nightly, merged only by a person, reversible, addressed by Health ID — feeds one interoperability layer — FHIR ingest from partners, the patient's own FHIR export, ICD-10, LOINC, ATC and UCUM as data, scoped credentials and replayable webhooks, aggregate reporting to DHIS2 once it is opened — which serves the workflows that already exist: triage, queue, prescribing, dispensing, sample-to-release, imaging reports and blood requests. The patient's phone talks to the same spine the pharmacy counter talks to.
The patient's record does not need transcribing at any desk, because every desk is on the same record.
OpesCare is built by OPES Health Systems for care in Cameroon. It is available in English and French, for patients and for staff, on Android and the web today, with iOS on the roadmap. This essay is the companion to The Record Is the Product, which describes the patient's side. Read more about how hospitals join OpesCare, the Health ID, FHIR and the Master Patient Index, or the developer documentation.
Frequently asked questions
What is the difference between the OpesCare app and the OpesCare platform?
The app is what a patient — or a clinician on the move — holds in their hand. The platform is the web application at opescare.com that institutions use: pharmacy, laboratory, imaging, blood-bank, front-desk and administration consoles, plus the API the app and partners talk to. Both work on the same record.
How does OpesCare avoid creating duplicate patients?
Registration and booking at the desk use the exact Health ID, and every patient-creation path checks for an existing identity first. A nightly scan raises likely duplicates for review at the Master Patient Index desk. A person decides each pair; merges are logged and reversible.
Does OpesCare exchange data with hospital systems using FHIR?
Inbound, yes: approved partners can send FHIR R4 diagnostic reports, observations, immunizations and transactions with scoped credentials. OpesCare does not run an outbound FHIR server. Patients can export their own record as a FHIR bundle from the app.
Can a pharmacy manage its stock in OpesCare?
Yes, for what it needs to be findable and to dispense: stock levels, batches, receipts of incoming stock, adjustments, reorder notes, price lists and spreadsheet import. Purchase orders, procurement and supplier contracts are out of scope.
Who is told when a lab result is released?
Both the patient and the clinician who ordered the test, in the same step. A critical value is flagged as high priority, and the laboratory records its telephone call-back to the clinician.
Does OpesCare send data to DHIS2?
Only aggregate counts, never patient-level data, and only after a platform administrator approves each monthly draft. Drafts are generated today; nothing is sent until MINSANTE provides access to the national instance.
Does OpesCare handle insurance claims or hospital admissions?
No. Insurance claims, pre-authorisation, admissions, discharges, transfers and bed management workflows are deliberately out of scope. A ward and bed register exists only so a facility can count occupancy.
Can a developer integrate with OpesCare today?
Yes. The developer portal issues application credentials, lets you subscribe to webhooks, inspect deliveries and replay them, and the documentation at opescare.com/docs covers the API, authentication, webhooks and the consent, QR, FHIR and MPI flows.
Sources
- Platform architecture — OpesCare documentation — OpesCare (accessed 19 October 2026)
- Master Patient Index flow — OpesCare documentation — OpesCare (accessed 19 October 2026)
- FHIR R4 ingest flow — OpesCare documentation — OpesCare (accessed 19 October 2026)
- Consent flow — OpesCare documentation — OpesCare (accessed 19 October 2026)
- Webhooks — OpesCare documentation — OpesCare (accessed 19 October 2026)
- HL7 FHIR Release 4 specification — HL7 International (accessed 19 October 2026)
- LOINC — Logical Observation Identifiers Names and Codes — Regenstrief Institute (accessed 19 October 2026)
- International Classification of Diseases, 10th revision (ICD-10) — World Health Organization (accessed 19 October 2026)
- ATC/DDD Index — WHO Collaborating Centre for Drug Statistics Methodology (accessed 19 October 2026)
- The Unified Code for Units of Measure (UCUM) — Regenstrief Institute (accessed 19 October 2026)
Share this article
Find care with OpesCare
Free, in English and French, across Cameroon.
Related articles
OpesCare: Every Module and the Problem It Solves
A complete guide to OpesCare's modules for patients, hospitals, pharmacies, laboratories, imaging centres, blood banks and developers in Cameroon.
Health ID, Interoperability and MPI in Cameroon, Explained
How OpesCare's Health ID, consent-based access, FHIR R4 ingest and Master Patient Index work, plus the Blood Finder, Drug Finder and Facility Directory.
How to Find a Medicine in Stock Near You in Cameroon
A step-by-step guide to finding a medicine at a licensed pharmacy near you in Cameroon with the OpesCare Medicine Finder, and why it beats the street.
This article is general information, not medical advice. In an emergency, go to the nearest hospital.
No comments yet
Be the first to comment. Comments are reviewed before they appear.
Leave a comment
All comments are moderated.