Skip to main content

Building a Patient Knowledge Library

Use case

A diabetes management app for patients needs a curated library of educational content. Practitioners at the clinic create and organize articles about nutrition, exercise guides with embedded images, and video tutorials on insulin injection technique. Patients browse this library from the app to learn about managing their condition.

Some content is general - available to every patient of the clinic (for example, a guide on healthy eating). Other content is personalized: a practitioner assigns specific reading materials to a specific patient as part of their care plan (for example, a post-diagnosis orientation packet for a newly diagnosed patient).

Other scenarios this recipe applies to

  • Oncology treatment information: An oncology center provides treatment information sheets linked to specific diagnoses. Oncologists "prescribe" reading materials alongside chemotherapy orders so that patients receive the right information at the right stage of treatment.
  • Rehabilitation exercise libraries: A rehabilitation facility offers exercise video libraries linked to care plan activities. As the patient progresses through their recovery, new exercises are unlocked and assigned by the physiotherapist.

What you will build

Prerequisites

Choosing the right FHIR resources

Building a knowledge library in FHIR means solving three problems: storing content, organizing it for browsing, and delivering personalized content to individual patients. Each problem maps to a different FHIR resource.

DocumentReference: the content container

DocumentReference was designed to represent documents in an EHR system. It carries rich metadata - author, date, category, security labels, description - and supports multiple content attachments in different formats (for example, both a PDF and an HTML version of the same article). For a knowledge library, each article, guide, or video becomes one DocumentReference.

The actual file (the PDF, the video) is stored via $binary-upload to avoid bloating the FHIR database. The DocumentReference holds only the metadata and a firearrow:// URL pointing to the file in Azure Blob Storage.

Categorizing content for browsing

DocumentReference has two key fields for organizing content:

  • category - broad grouping, like "education", "nutrition", "exercise", "medication-information". Patients use this to browse by topic.
  • type - specific document type, like "patient information leaflet", "video tutorial", "exercise instruction". This helps distinguish formats within a category.

Together, these fields create a browsable taxonomy. A client app can search by category to show a "Nutrition" section, or filter by type to show only video content.

description provides a searchable summary of the content, useful for full-text search in the client app.

Communication: delivering content to a patient

When a practitioner wants to assign a specific article to a specific patient, they create a Communication resource. Communication represents a message or content delivery that has occurred. Its payload.contentReference points to the DocumentReference, and its recipient points to the patient.

This creates a clear record: "Dr. Smith sent the Nutrition Guide to Patient Jane Doe on April 4, 2026." The patient's app can query for Communications addressed to them to show a "My Assigned Reading" section.

ActivityDefinition: making content prescribable

If the same content item is assigned to many patients (for example, every newly diagnosed diabetes patient receives the Nutrition Guide), creating Communications manually each time is tedious. An ActivityDefinition of kind CommunicationRequest acts as a reusable template. It says: "when triggered, create a CommunicationRequest that delivers this DocumentReference to the subject patient."

ActivityDefinitions can be linked from PlanDefinition actions, which means content delivery can become part of a standardized care protocol.

PlanDefinition: bundling content into a program

A PlanDefinition lets you bundle multiple ActivityDefinitions into a structured educational program with timing. For example, a "Newly Diagnosed Diabetes" PlanDefinition could include:

  • Week 1: Send Nutrition Guide
  • Week 2: Send Exercise Guide
  • Week 3: Send Insulin Injection Technique Video

When this PlanDefinition is applied to a patient ($apply), it generates a patient-specific CarePlan with the scheduled content deliveries.

Why not FHIR Library?

FHIR's Library resource might seem like a natural fit for a "library" of content, but it is designed for Clinical Quality Language (CQL) logic libraries and clinical knowledge artifacts - not for patient-facing educational content. While Library has an asset-collection type that could technically group DocumentReferences, it adds complexity without meaningful benefit.

For organizing content, DocumentReference categories and FHIR search queries are simpler and more effective. Use GET /fhir/DocumentReference?category=education to browse the library - no Library resource needed.

Securing access

Educational content raises specific access control questions:

  1. General content (the overall library) should be readable by all patients of the clinic. A patient at Clinic A should see Clinic A's content library, but not Clinic B's materials. This is an organizational access pattern.
  2. Personalized assignments (Communications) contain PHI - they reveal which specific content was assigned to which patient. A patient should see only Communications addressed to them.
  3. Practitioners need to create and manage content for their organization, and create Communications to assign content to patients.

Complete authorization rules

fire-arrow:
authorization:
default-validator: Forbidden
validation-rules:
# --- Content library access ---

# Patients can browse all educational content in their managing organization
- client-role: Patient
resource: DocumentReference
operation: read
validator: LegitimateInterest
- client-role: Patient
resource: DocumentReference
operation: search
validator: LegitimateInterest
- client-role: Patient
resource: DocumentReference
operation: graphql-read
validator: LegitimateInterest
- client-role: Patient
resource: DocumentReference
operation: graphql-search
validator: LegitimateInterest

# Patients can read Communications addressed to them (personalized assignments)
- client-role: Patient
resource: Communication
operation: read
validator: PatientCompartment
- client-role: Patient
resource: Communication
operation: search
validator: PatientCompartment

# Patients can upload files (e.g., for communication back to practitioners)
- client-role: Patient
resource: Binary
operation: binary-upload
validator: PatientCompartment

# Patient identity
- client-role: Patient
resource: Patient
operation: read
validator: PatientCompartment
- client-role: Patient
resource: Patient
operation: me
validator: PatientCompartment

# --- Practitioner content management ---

# Practitioners manage the content library for their organization
- client-role: Practitioner
resource: DocumentReference
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: DocumentReference
operation: search
validator: LegitimateInterest
- client-role: Practitioner
resource: DocumentReference
operation: create
validator: LegitimateInterest
- client-role: Practitioner
resource: DocumentReference
operation: update
validator: LegitimateInterest
- client-role: Practitioner
resource: DocumentReference
operation: delete
validator: LegitimateInterest

# Practitioners can assign content to patients via Communication
- client-role: Practitioner
resource: Communication
operation: create
validator: LegitimateInterest
- client-role: Practitioner
resource: Communication
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: Communication
operation: search
validator: LegitimateInterest

# Practitioners upload files for content items
- client-role: Practitioner
resource: Binary
operation: binary-upload
validator: LegitimateInterest

# Practitioners manage ActivityDefinitions and PlanDefinitions
- client-role: Practitioner
resource: ActivityDefinition
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: ActivityDefinition
operation: search
validator: LegitimateInterest
- client-role: Practitioner
resource: ActivityDefinition
operation: create
validator: LegitimateInterest
- client-role: Practitioner
resource: ActivityDefinition
operation: update
validator: LegitimateInterest

- client-role: Practitioner
resource: PlanDefinition
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: PlanDefinition
operation: search
validator: LegitimateInterest
- client-role: Practitioner
resource: PlanDefinition
operation: create
validator: LegitimateInterest
- client-role: Practitioner
resource: PlanDefinition
operation: update
validator: LegitimateInterest

# Practitioners can read patients (needed for $apply and content assignment)
- client-role: Practitioner
resource: Patient
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: Patient
operation: search
validator: LegitimateInterest
Why LegitimateInterest for DocumentReference reads by patients?

The content library is organizational, not patient-specific. A DocumentReference for an educational article is not "about" a specific patient - it belongs to the organization. Using LegitimateInterest means patients can see all DocumentReferences linked to their managing organization, which is exactly the right scope for a shared content library.

For Communications (personalized assignments), PatientCompartment ensures each patient sees only their own assignments.

Step-by-step instructions

Step 1: Upload content files

Upload the actual files (PDFs, images, videos) using $binary-upload. If you are not familiar with this process, see the Media and Binary Storage recipe for a detailed walkthrough.

Upload a PDF article:

curl -X POST http://localhost:8080/fhir/\$binary-upload \
-H "Authorization: Bearer <practitioner-token>" \
-F "resourceReference=Organization/clinic-a" \
-F "[email protected]"

Upload a video tutorial:

curl -X POST http://localhost:8080/fhir/\$binary-upload \
-H "Authorization: Bearer <practitioner-token>" \
-F "resourceReference=Organization/clinic-a" \
-F "[email protected]"
tip

For large video files, increase the max-file-size setting in your binary storage configuration. Videos can easily exceed the default 10 MB limit. A value like 104857600 (100 MB) is reasonable for clinical education videos.

Save the returned firearrow:// URLs for use in the next step.

Step 2: Create DocumentReference resources

Create a DocumentReference for each content item. Here is a complete example for a nutrition guide:

curl -X POST http://localhost:8080/fhir/DocumentReference \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "DocumentReference",
"status": "current",
"type": {
"coding": [{
"system": "http://loinc.org",
"code": "69981-9",
"display": "Patient education material"
}]
},
"category": [{
"coding": [{
"system": "http://example.org/content-category",
"code": "nutrition",
"display": "Nutrition"
}]
}],
"description": "Comprehensive guide to nutrition for patients with Type 2 Diabetes. Covers carbohydrate counting, glycemic index, meal planning, and practical recipes.",
"author": [{
"reference": "Practitioner/dr-smith"
}],
"content": [{
"attachment": {
"contentType": "application/pdf",
"url": "firearrow://fhir-attachments/Organization/clinic-a/abc123-nutrition-guide.pdf",
"title": "Nutrition Guide for Type 2 Diabetes",
"language": "en"
}
}]
}'

In the Web UI, open the Resource Browser, select DocumentReference, and click Create. Fill in the same fields in the JSON editor or the structured form.

Create additional DocumentReferences for the exercise guide and the insulin video, using appropriate categories (exercise, medication-technique) and MIME types (application/pdf, video/mp4).

Step 3: Organize with categories

With multiple content items created, patients can browse by category:

# Browse all educational content
curl "http://localhost:8080/fhir/DocumentReference?type=69981-9" \
-H "Authorization: Bearer <patient-token>"

# Browse nutrition content only
curl "http://localhost:8080/fhir/DocumentReference?type=69981-9&category=nutrition" \
-H "Authorization: Bearer <patient-token>"

# Search by description text
curl "http://localhost:8080/fhir/DocumentReference?type=69981-9&description:contains=diabetes" \
-H "Authorization: Bearer <patient-token>"

The LegitimateInterest validator automatically narrows results to content from the patient's managing organization. A patient at Clinic A sees only Clinic A's library.

Step 4: Assign content to a patient directly

When a practitioner wants to assign a specific article to a patient, create a Communication resource:

curl -X POST http://localhost:8080/fhir/Communication \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "Communication",
"status": "completed",
"category": [{
"coding": [{
"system": "http://example.org/comm-category",
"code": "education",
"display": "Patient Education"
}]
}],
"subject": {
"reference": "Patient/patient-123"
},
"recipient": [{
"reference": "Patient/patient-123"
}],
"sender": {
"reference": "Practitioner/dr-smith"
},
"sent": "2026-04-04T14:00:00Z",
"payload": [{
"contentReference": {
"reference": "DocumentReference/nutrition-guide-001"
}
}]
}'

The patient's app can then query for assigned content:

# Get all content assigned to this patient
curl "http://localhost:8080/fhir/Communication?recipient=Patient/patient-123&category=education" \
-H "Authorization: Bearer <patient-token>"

The PatientCompartment validator ensures patients see only Communications addressed to them.

Step 5: Make content prescribable with ActivityDefinition

For content that is frequently assigned to patients, create an ActivityDefinition to avoid manual Communication creation each time:

curl -X POST http://localhost:8080/fhir/ActivityDefinition \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "ActivityDefinition",
"url": "http://example.org/activity/send-nutrition-guide",
"name": "SendNutritionGuide",
"title": "Send Nutrition Guide for Type 2 Diabetes",
"status": "active",
"kind": "CommunicationRequest",
"description": "Sends the Type 2 Diabetes Nutrition Guide to the patient.",
"code": {
"coding": [{
"system": "http://example.org/activity-code",
"code": "patient-education",
"display": "Patient Education Delivery"
}]
}
}'

This ActivityDefinition can now be referenced from PlanDefinition actions. When the PlanDefinition is applied, the server generates a CommunicationRequest for the patient.

Step 6: Build a content delivery program with PlanDefinition

Create a PlanDefinition that delivers educational content on a schedule. In the Web UI, open Plan Definitions and click + New Plan Definition.

Or create it via the API:

curl -X POST http://localhost:8080/fhir/PlanDefinition \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "PlanDefinition",
"url": "http://example.org/plan/diabetes-education-program",
"title": "Newly Diagnosed Diabetes - Education Program",
"status": "active",
"type": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/plan-definition-type",
"code": "clinical-protocol"
}]
},
"description": "Four-week educational program for newly diagnosed Type 2 Diabetes patients. Delivers one content item per week.",
"action": [
{
"id": "week-1-nutrition",
"title": "Week 1: Nutrition Guide",
"description": "Send the nutrition guide to the patient.",
"definitionCanonical": "http://example.org/activity/send-nutrition-guide",
"timingDuration": {
"value": 0,
"unit": "d",
"system": "http://unitsofmeasure.org",
"code": "d"
}
},
{
"id": "week-2-exercise",
"title": "Week 2: Exercise Guide",
"description": "Send the exercise guide to the patient.",
"definitionCanonical": "http://example.org/activity/send-exercise-guide",
"timingDuration": {
"value": 7,
"unit": "d",
"system": "http://unitsofmeasure.org",
"code": "d"
}
},
{
"id": "week-3-insulin",
"title": "Week 3: Insulin Technique Video",
"description": "Send the insulin injection technique video.",
"definitionCanonical": "http://example.org/activity/send-insulin-video",
"timingDuration": {
"value": 14,
"unit": "d",
"system": "http://unitsofmeasure.org",
"code": "d"
}
}
]
}'

In the Plan Definition editor, you can build this visually: add actions, link each to its ActivityDefinition, and set the timing offset using the Duration timing mode.

Step 7: Apply the program to a patient

When a new patient is diagnosed with diabetes, apply the PlanDefinition to generate a patient-specific CarePlan.

In the Web UI: open the PlanDefinition, click the Apply icon, select the patient, and confirm.

Via the API:

curl -X POST "http://localhost:8080/fhir/PlanDefinition/diabetes-education-plan/\$apply" \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "Parameters",
"parameter": [
{ "name": "subject", "valueString": "Patient/patient-123" },
{ "name": "_persist", "valueBoolean": true },
{ "name": "_startDate", "valueDate": "2026-04-04" },
{ "name": "_timezone", "valueString": "Europe/Zurich" }
]
}'

The server returns a CarePlan with activities for each content delivery, scheduled at the appropriate offsets from the start date. See Custom Operations for the full $apply reference.

Step 8: Query the library from a client app

Here are the most common queries a client app needs:

Browse the full library:

import requests

base_url = "http://localhost:8080/fhir"
headers = {"Authorization": "Bearer <patient-token>"}

# Get all educational content
response = requests.get(
f"{base_url}/DocumentReference",
params={"type": "69981-9"},
headers=headers
)
library = response.json()

for entry in library.get("entry", []):
doc = entry["resource"]
print(f"- {doc['content'][0]['attachment']['title']}")
print(f" Category: {doc['category'][0]['coding'][0]['display']}")
print(f" {doc['description']}")

Get a patient's assigned content:

response = requests.get(
f"{base_url}/Communication",
params={
"recipient": "Patient/patient-123",
"category": "education",
"_sort": "-sent"
},
headers=headers
)

Retrieve a specific document with its download URL:

response = requests.get(
f"{base_url}/DocumentReference/nutrition-guide-001",
headers=headers
)
doc = response.json()

# The firearrow:// URL has been replaced with a time-limited pre-signed URL
download_url = doc["content"][0]["attachment"]["url"]
# Download the file immediately (URL expires after the configured timeout)
file_response = requests.get(download_url)

Further reading