Skip to main content

Consent Lifecycle

Statesโ€‹

A ConsentRecord moves through FHIR-aligned states:

draft โ”€โ”€โ–บ active โ”€โ”€โ–บ inactive   (revoked or expired)
โ”‚
โ””โ–บ rejected (patient declined)

Only active records are evaluated by the enforcement interceptor. Records in any other state are ignored โ€” if a patient revokes and later re-authorises, a new active record is created; the old inactive record is preserved for the audit trail.

Via REST APIโ€‹

POST /api/consent
Authorization: Bearer <clinician-token>
Content-Type: application/json

{
"patientId": "Patient/patient-123",
"actorReference": "Device/my-smart-app",
"provisionType": "permit",
"resourceClasses": ["Observation", "Patient"],
"scopeValues": ["patient/Observation.rs", "patient/Patient.rs"],
"periodStart": "2025-01-01",
"periodEnd": "2027-12-31",
"regulatoryBasis": "GDPR Art.9",
"note": "Verbal consent recorded"
}

permittedOperations is derived automatically from scopeValues. You do not need to set it explicitly. patient/Observation.rs produces permittedOperations = "rs".

Both SMART v1 and SMART v2.2 scope formats are accepted:

Scope formatpermittedOperations
patient/Observation.rsrs (SMART v2.2)
patient/Observation.readrs (SMART v1 โ€” read + search)
patient/Observation.writecud (SMART v1 โ€” create + update + delete)
patient/Observation.crudscruds
patient/Observation.*"" (wildcard โ€” all operations)

Via FHIR API directlyโ€‹

You can write a FHIR Consent resource directly to the HAPI server. ConsentFhirWriteInterceptor detects the write and creates the corresponding JPA record automatically.

Via patient portalโ€‹

Patients can grant and edit consents from the portal at /consent/portal/grant and /consent/portal/edit/{id}. The portal derives SMART scope strings automatically from the resource types the patient selects.

Evaluation algorithmโ€‹

ConsentService.evaluate() is called on every FHIR request.

Input: patientId, actorReference, resourceType, FhirOperation

Step 1 โ€” Actor-specific:
findActiveForPatientAndActor(patientId, actorReference)
โ†’ filter: isEffective() [status=active + period valid in UTC]
โ†’ filter: coversResource(type) [empty list = wildcard]
โ†’ filter: coversOperation(op) [permission letter check]
โ†’ first match โ†’ result

Step 2 โ€” Patient-level fallback:
findActiveForPatient(patientId)
โ†’ filter: actorReference IS NULL [patient-wide record]
โ†’ same filters
โ†’ first match โ†’ result

Step 3 โ€” User-context fallback:
findActiveUserContextForActor(actorReference)
โ†’ WHERE scopeContext = 'user' [SMART v2.2 clinician-level access]
โ†’ same filters
โ†’ first match โ†’ result

Step 4 โ€” System-context fallback:
findActiveSystemContextForActor(actorReference)
โ†’ WHERE scopeContext = 'system' [backend SMART services, no patient context]
โ†’ same filters
โ†’ first match โ†’ result

Step 5 โ€” Deny by default
system/ scope

system/ scope records have patientId = null and scopeContext = 'system'. They grant the actor access to any patient โ€” appropriate for backend integrations such as bulk export or analytics pipelines. Evaluated last so patient-specific and clinician-level records always take priority.

Operation letter mappingโ€‹

HTTP requestHAPI RestOperationTypeLetter
GET /Observation/{id}READr
GET /Observation?patient=XSEARCH_TYPEs
POST /ObservationCREATEc
PUT /Observation/{id}UPDATEu
PATCH /Observation/{id}PATCHu
DELETE /Observation/{id}DELETEd

A consent with permittedOperations = "rs" permits reads and searches but denies create, update, and delete โ€” even if the SMART scope token technically allows them.

Partial update โ€” only non-null fields are applied:

PUT /api/consent/{id}
Authorization: Bearer <clinician-token>

{
"resourceClasses": ["Observation"],
"scopeValues": ["patient/Observation.r"],
"note": "Narrowed to read-only"
}

scopeValues automatically re-derives permittedOperations. The change is synced to HAPI FHIR asynchronously.

POST /api/consent/{id}/revoke?reason=Patient+requested+revocation
Authorization: Bearer <clinician-token>

Revocation sets status = inactive. The record is never deleted โ€” it is preserved permanently in the audit trail for regulatory compliance. A FHIR Consent resource update is synced to HAPI.

After revocation, any FHIR request for that patient and actor combination will be denied until a new active consent is created.

The patient can also revoke from the portal at /consent/portal/revoke/{id}.

ConsentRecord.isEffective() checks the period on every evaluation using UTC to ensure deterministic behaviour regardless of server timezone:

LocalDate today = LocalDate.now(ZoneOffset.UTC);
if (periodEnd != null && today.isAfter(periodEnd)) return false;

Expired consents are not deleted. They remain in the database with status = active but isEffective() returns false โ€” the decision engine filters them out transparently. The compliance team can query expired consents for renewal outreach.

On-demand evaluationโ€‹

External systems can query the consent decision without making a FHIR request:

POST /api/consent/evaluate
Authorization: Bearer <system-token>

?patientId=Patient/patient-123
&actorReference=Device/my-smart-app
&resourceType=Observation
&fhirOperation=READ

This endpoint requires ROLE_SYSTEM or ROLE_ADMIN. Every call is audit-logged.


Next: Patient Portal โ†’