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.
Creating consentโ
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 format | permittedOperations |
|---|---|
patient/Observation.rs | rs (SMART v2.2) |
patient/Observation.read | rs (SMART v1 โ read + search) |
patient/Observation.write | cud (SMART v1 โ create + update + delete) |
patient/Observation.cruds | cruds |
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 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 request | HAPI RestOperationType | Letter |
|---|---|---|
GET /Observation/{id} | READ | r |
GET /Observation?patient=X | SEARCH_TYPE | s |
POST /Observation | CREATE | c |
PUT /Observation/{id} | UPDATE | u |
PATCH /Observation/{id} | PATCH | u |
DELETE /Observation/{id} | DELETE | d |
A consent with permittedOperations = "rs" permits reads and searches but denies create, update, and delete โ even if the SMART scope token technically allows them.
Updating consentโ
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.
Revoking consentโ
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}.
Consent expiryโ
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 โ