Skip to content
OpesCare Developers

Architecture and flows

Consent and scoped access

A Health ID tells a clinician who a patient is, not what is in their record. Reading the record needs a basis; when there is none, the clinician asks and the patient decides.

The flow

Scroll through the steps below the diagram, or hover a step, to highlight it in the diagram.

Consent and scoped access A clinician asks, the patient approves in the app, access is scoped and time-limited, every read is logged, and the patient can revoke. Clinician web or app Look up Health ID (scan or type) OpesCare API Basis for reading? relationship or grant Request consent: scope + reason OpesCare API Scoped data returned Clinician Access logged Clinical record Gate checks basis, scoped read OpesCare API Notify patient Patient Further reads refused revoked or expired Grant: scoped, expires_at set Clinical record Approve (or deny) Patient Request denied the chart stays closed no basis basis held approve deny open chart admitted Person or device OpesCare service Stored data Gate (check) Decision Outcome: done Outcome: refused Request Response Queued / async Refusal or failure
  1. Look up Health ID (scan or type) The clinician scans the patient QR or enters the Health ID.
  2. Identity only, no chart Without an appointment, care-team link or grant, the record stays closed.
  3. Request consent: scope + reason The request names what will be read and why.
  4. Notify patient The request appears in the patient app with a push notification.
  5. Approve (or deny) The patient decides. A denial ends the flow here.
  6. Grant: scoped, expires_at set A consent grant is stored with its scope and expiry time.
  7. Open the chart The clinician reads the record.
  8. Gate checks basis, scoped read The clinical relationship gate admits only what the grant covers.
  9. Access logged Who read what, when and on which basis is written to the access log the patient can see.
  10. Scoped data returned Only the granted part of the record comes back.
  11. Revoke, or grant expires The patient can revoke at any time; otherwise the grant lapses at its expiry.
  12. 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
CardEvidence 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

All flows as JSON

What this guarantees

  • No basis, no chart: a Health ID lookup never opens a record by itself.
  • Access is limited to the granted scope and ends at expiry or revocation.
  • Every read is logged and visible to the patient.

Where it lives in the code

  • config:staff_capabilities.capabilities.identify_patient
  • route:portals.chart.consent_request
  • uri:POST api/mobile/doctor/patients/scan
  • method:App\Http\Controllers\Api\Mobile\MobileProviderScanController::scan
  • method:App\Support\ClinicalRelationshipGate::evaluate
  • method:App\Support\ClinicalRelationshipGate::holds
  • uri:POST api/mobile/doctor/consent-requests
  • method:App\Http\Controllers\Api\Mobile\MobileConsentRequestController::store
  • config:staff_capabilities.capabilities.request_access
  • uri:GET api/mobile/doctor/patients/{patient}
  • method:App\Http\Controllers\Api\Mobile\MobileDoctorChartController::show
  • class:App\Models\AccessLog
  • uri:GET api/mobile/access-logs
  • method:App\Support\ClinicalRelationshipGate::hasActiveConsentGrant
  • method:App\Support\ClinicalRelationshipGate::coversFullRecord
  • uri:GET api/mobile/consent-requests
  • uri:GET api/mobile/consent-requests/pending-count
  • uri:POST api/mobile/consents/{id}/revoke
  • method:App\Http\Controllers\Api\Mobile\MobileGovernanceController::revokeConsent
  • class:App\Models\ConsentGrant
  • method:App\Http\Controllers\Api\Mobile\MobileGovernanceController::approveConsent
  • uri:POST api/mobile/consent-requests/{id}/approve
  • uri:POST api/mobile/consent-requests/{id}/deny
  • method:App\Http\Controllers\Api\Mobile\MobileGovernanceController::denyConsent