<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: mock health</title>
    <description>The latest articles on Forem by mock health (@mockhealth).</description>
    <link>https://forem.com/mockhealth</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3838912%2F91ebfbe2-b5be-4786-8f4c-cf10f617d99c.png</url>
      <title>Forem: mock health</title>
      <link>https://forem.com/mockhealth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mockhealth"/>
    <language>en</language>
    <item>
      <title>Testing FHIR Integrations Without a Hospital</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Tue, 14 Apr 2026 21:24:38 +0000</pubDate>
      <link>https://forem.com/mockhealth/testing-fhir-integrations-without-a-hospital-i2k</link>
      <guid>https://forem.com/mockhealth/testing-fhir-integrations-without-a-hospital-i2k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3rnf709xqttfykcetss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3rnf709xqttfykcetss.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
You're building a FHIR integration. You need to test it against data that looks like what a hospital actually produces.&lt;/p&gt;

&lt;p&gt;You don't have access to a hospital.&lt;/p&gt;

&lt;p&gt;This is the catch-22 that every FHIR startup lives in for 6-12 months. You can't get production EHR access without a working, tested integration. You can't build one without production-quality data. Epic's app marketplace review takes 2-4 months. Oracle Health takes 3-6. And all of them want evidence that your software works &lt;em&gt;before&lt;/em&gt; they give you the data to prove it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Testing Pyramid for FHIR
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Tier 1: Parse and Validate
&lt;/h3&gt;

&lt;p&gt;Does your FHIR output conform to US Core profiles? You don't need a server for this. The &lt;a href="https://hapifhir.io/hapi-fhir/docs/validation/instance_validator.html" rel="noopener noreferrer"&gt;HAPI FHIR Validator&lt;/a&gt; runs locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; validator_cli.jar patient-bundle.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ig&lt;/span&gt; hl7.fhir.us.core#6.1.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-profile&lt;/span&gt; http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in CI on every commit. It catches malformed resources, missing required fields, and profile violations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: Integration
&lt;/h3&gt;

&lt;p&gt;This requires a running FHIR server with SMART on FHIR auth. You need to test the full request lifecycle: discovery, authorization, token exchange, scoped queries.&lt;/p&gt;

&lt;p&gt;Things that break at this tier and nowhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your search uses &lt;code&gt;Observation?category=laboratory&lt;/code&gt; but the server indexes it as &lt;code&gt;Observation?category=http://terminology.hl7.org/CodeSystem/observation-category|laboratory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your SMART scopes request &lt;code&gt;patient/Observation.read&lt;/code&gt; but the server only grants &lt;code&gt;patient/Observation.rs&lt;/code&gt; — read and search are separate grants&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Patient/$everything&lt;/code&gt; returns 2,000 entries with pagination links your client doesn't follow&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 3: Realistic
&lt;/h3&gt;

&lt;p&gt;This is the tier most teams skip. You need patients that look like real patients:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you're testing&lt;/th&gt;
&lt;th&gt;What the data needs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lab trending UI&lt;/td&gt;
&lt;td&gt;3+ years of longitudinal labs with reference ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medication reconciliation&lt;/td&gt;
&lt;td&gt;8-12 active medications with start dates and dosages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Problem list display&lt;/td&gt;
&lt;td&gt;5-15 active conditions with SNOMED coding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prior auth workflow&lt;/td&gt;
&lt;td&gt;Complex patients who actually get denied&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your Tier 3 data is a single patient named "Test Cancer" with one condition, your tests aren't testing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Local HAPI + Synthea
&lt;/h3&gt;

&lt;p&gt;You've probably already done this. HAPI in Docker, a handful of Synthea patients, maybe a script that generates 10 bundles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 hapiproject/hapi:latest
&lt;span class="nb"&gt;cd &lt;/span&gt;synthea &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./run_synthea &lt;span class="nt"&gt;-p&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When this is enough:&lt;/strong&gt; Early prototyping, Tier 1 validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it isn't:&lt;/strong&gt; No SMART auth. Synthea defaults produce patients with single conditions and no clinical depth. At some point the test data pipeline becomes its own project — one you maintain alongside the product you're actually building.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Vendor Sandboxes
&lt;/h3&gt;

&lt;p&gt;Open Epic, Oracle Health Code, SMART Health IT Sandbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When this is enough:&lt;/strong&gt; Testing OAuth against a real vendor's auth server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it isn't:&lt;/strong&gt; Open Epic gives you 8 patients with sparse data. No comorbidity patterns. No longitudinal labs. No imaging. No clinical notes. &lt;a href="https://mock.health/blog/fhir-sandbox-problem" rel="noopener noreferrer"&gt;We wrote a whole post about this.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Clinically Realistic Sandbox
&lt;/h3&gt;

&lt;p&gt;A FHIR server with SMART auth and patients generated from real clinical patterns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.mock.health/fhir/Condition?patient&lt;span class="o"&gt;=&lt;/span&gt;example &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.entry[].resource.code.coding[0].display'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Type 2 diabetes mellitus"
"Essential hypertension"
"Chronic kidney disease, stage 3"
"Hyperlipidemia"
"Diabetic retinopathy"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These conditions travel together because the generation engine learned that pattern from real CMS claims data. Compare to Open Epic: &lt;code&gt;"Pain in throat"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes When You Get Production Access
&lt;/h2&gt;

&lt;p&gt;Your sandbox-tested code won't ship unmodified. You'll encounter vendor-specific extensions, data quality variance (&lt;a href="https://mock.health/blog/these-dumpster-contents-are-private" rel="noopener noreferrer"&gt;plan for trash&lt;/a&gt;), and an opaque app review process.&lt;/p&gt;

&lt;p&gt;But the architecture you built against a sandbox will survive. The specific data handling will need adaptation. That's normal — sandbox testing builds the 90% that doesn't depend on vendor-specific behavior.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;HAPI Validator in CI&lt;/td&gt;
&lt;td&gt;Malformed resources, profile violations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SMART-enabled sandbox&lt;/td&gt;
&lt;td&gt;OAuth bugs, search parameter issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Realistic synthetic data&lt;/td&gt;
&lt;td&gt;Logic errors with complex patients&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run Tier 1 on every commit. Run Tier 2 when you change auth or query logic. Run Tier 3 before every demo and before you apply for production access.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; — SMART-enabled FHIR sandbox with clinical density to get through Tier 3 testing. &lt;a href="https://mock.health/login" rel="noopener noreferrer"&gt;API key in 60 seconds →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fhir</category>
      <category>healthtech</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Three Ways to Build a Multi-Tenant FHIR Server</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Sun, 12 Apr 2026 23:35:03 +0000</pubDate>
      <link>https://forem.com/mockhealth/three-ways-to-build-a-multi-tenant-fhir-server-2jd</link>
      <guid>https://forem.com/mockhealth/three-ways-to-build-a-multi-tenant-fhir-server-2jd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkvqm1bf09f0e8274b4j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkvqm1bf09f0e8274b4j.png" alt=" " width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You're building a FHIR platform that serves multiple customers. Each one needs isolated data through the same API. There's no best practice for this — Darren Devitt's &lt;a href="https://darrendevitt.com" rel="noopener noreferrer"&gt;&lt;em&gt;FHIR Architecture Decisions&lt;/em&gt;&lt;/a&gt; calls multi-tenancy a "deal breaker" requirement that can eliminate entire classes of FHIR servers.&lt;/p&gt;

&lt;p&gt;Three models exist. We've built two of them in production with &lt;a href="https://github.com/hapifhir/hapi-fhir" rel="noopener noreferrer"&gt;HAPI FHIR&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model 1: Separate Database Per Tenant
&lt;/h2&gt;

&lt;p&gt;Each tenant gets their own database schema — or for true physical isolation, their own database host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Strongest isolation. Independent scaling. &lt;code&gt;DROP SCHEMA&lt;/code&gt; to delete a tenant. Compliance teams love it (especially separate hosts).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt; You're managing N connection pools, N migrations, N backups. HAPI's HikariCP defaults to 10 connections — we exhausted the pool with 8 concurrent workers and left HAPI in a zombie state. Cold starts per tenant add 30-45 seconds when a database hasn't been accessed recently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; Regulations mandate physical separation. Tenants are large enough to justify the overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model 2: Shared Database, Partitioned Tables
&lt;/h2&gt;

&lt;p&gt;One database, but every resource row gets a &lt;code&gt;PARTITION_ID&lt;/code&gt; column. HAPI supports this natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;partitioning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;database_partition_mode_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;request_tenant_partitioning_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;allow_references_across_partitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating a partition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /fhir/$partition-management-create-partition
{"resourceType": "Parameters", "parameter": [
  {"name": "id", "valueInteger": 101},
  {"name": "name", "valueCode": "tenant_acme"}
]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Logical isolation at the storage layer. One database to manage. Cross-partition references for shared data (practitioner directories, formularies).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt; HAPI bugs surface here. &lt;a href="https://github.com/hapifhir/hapi-fhir/issues/1099" rel="noopener noreferrer"&gt;#1099&lt;/a&gt;: &lt;code&gt;patient=UUID&lt;/code&gt; triggers "Non-unique ID" errors — fix by rewriting to &lt;code&gt;patient:Patient._id=UUID&lt;/code&gt;. &lt;a href="https://github.com/hapifhir/hapi-fhir/issues/6665" rel="noopener noreferrer"&gt;#6665&lt;/a&gt;: bulk export background jobs lose partition context entirely — we rebuilt the Bulk Data Access IG in Python.&lt;/p&gt;

&lt;p&gt;No built-in access control. HAPI doesn't know who's calling — your API gateway handles auth and routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; Many tenants, moderate data volumes. You want logical isolation without N databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model 3: Shared Database, Tag-Based Filtering
&lt;/h2&gt;

&lt;p&gt;One database, no partitions. Every resource gets a &lt;code&gt;meta.tag&lt;/code&gt; for its tenant. Your gateway appends &lt;code&gt;_tag=tenant_acme&lt;/code&gt; to every query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;extra_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://yourapp.com/tenant|&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Simplest to implement. Zero overhead per tenant. Works with any FHIR server, not just HAPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt; No real isolation — the boundary is your gateway's tag-filtering logic. A missed &lt;code&gt;_tag&lt;/code&gt; parameter on one endpoint leaks data across tenants.&lt;/p&gt;

&lt;p&gt;Worse: HAPI ignores &lt;code&gt;_tag&lt;/code&gt; on instance reads. &lt;code&gt;GET /fhir/Patient/abc-123&lt;/code&gt; returns the resource regardless of tags. You need post-fetch verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;resource_tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;resource_tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowed_tags&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; Prototyping. Internal tools. Small deployments. A starting point before migrating to partitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Separate DB&lt;/th&gt;
&lt;th&gt;Partitions&lt;/th&gt;
&lt;th&gt;Tags&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strongest&lt;/td&gt;
&lt;td&gt;Logical (storage)&lt;/td&gt;
&lt;td&gt;Logical (query)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compliance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Happy&lt;/td&gt;
&lt;td&gt;Usually OK&lt;/td&gt;
&lt;td&gt;Nervous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ops cost per tenant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Shared DB&lt;/td&gt;
&lt;td&gt;Shared everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HAPI bugs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fewer&lt;/td&gt;
&lt;td&gt;More&lt;/td&gt;
&lt;td&gt;Fewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works with any server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;HAPI-specific&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instance read safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inherent&lt;/td&gt;
&lt;td&gt;Inherent&lt;/td&gt;
&lt;td&gt;Post-fetch check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The direction of migration matters: &lt;strong&gt;you can move from tags → partitions → separate databases, but not easily the other way.&lt;/strong&gt; Start with the simplest model that meets your compliance requirements.&lt;/p&gt;

&lt;p&gt;Whichever model you choose, the FHIR server handles storage. Auth, routing, and access control are your gateway's job. &lt;a href="https://mock.health/blog/fhir-api-gateway" rel="noopener noreferrer"&gt;We wrote about what that gateway looks like.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; handles multi-tenant isolation so you can focus on your integration. Isolated FHIR data with SMART auth — no partition management required. &lt;a href="https://mock.health/login" rel="noopener noreferrer"&gt;Try it free →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fhir</category>
      <category>healthtech</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Building a FHIR API Gateway: What HAPI Won't Do for You</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Fri, 10 Apr 2026 22:56:33 +0000</pubDate>
      <link>https://forem.com/mockhealth/building-a-fhir-api-gateway-what-hapi-wont-do-for-you-1pll</link>
      <guid>https://forem.com/mockhealth/building-a-fhir-api-gateway-what-hapi-wont-do-for-you-1pll</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9zdu6qvdkddt2ivj3h4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9zdu6qvdkddt2ivj3h4.png" alt="HAPI stairs" width="800" height="998"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;HAPI is a FHIR storage engine. It validates resources, indexes search parameters, and returns Bundles. What it does not do: authenticate users, enforce per-tenant access control, generate correct URLs behind a reverse proxy, or stream large responses without buffering.&lt;/p&gt;

&lt;p&gt;We run FastAPI in front of HAPI on Google Cloud Run. Here are the five things the gateway does that HAPI can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Auth Injection: One Hook, Every Request
&lt;/h2&gt;

&lt;p&gt;In production, HAPI sits behind Cloud Run's IAM layer. Every request needs a GCP identity token. The pattern: an httpx event hook that fires before every outbound request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_inject_hapi_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_id_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audience_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug we hit: auth injection was per-route. The main proxy had it. Portal routes didn't. Everything worked locally (no IAM). Four endpoints returned 403 in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Auth belongs on the HTTP client, not on individual routes. One hook, one place, every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The &lt;code&gt;--proxy-headers&lt;/code&gt; Trap
&lt;/h2&gt;

&lt;p&gt;This will save you a day if you're running FastAPI behind any load balancer.&lt;/p&gt;

&lt;p&gt;Cloud Run terminates TLS. Your app gets HTTP. The load balancer passes &lt;code&gt;X-Forwarded-Proto: https&lt;/code&gt;. Uvicorn ignores this by default. So &lt;code&gt;request.base_url&lt;/code&gt; returns &lt;code&gt;http://&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cascade:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SMART discovery advertises &lt;code&gt;http://&lt;/code&gt; token endpoint&lt;/li&gt;
&lt;li&gt;Client POSTs auth code to &lt;code&gt;http://&lt;/code&gt; URL&lt;/li&gt;
&lt;li&gt;Cloud Run 302 redirects HTTP → HTTPS&lt;/li&gt;
&lt;li&gt;302 converts POST to GET (per HTTP spec)&lt;/li&gt;
&lt;li&gt;Token endpoint gets GET → &lt;strong&gt;405 Method Not Allowed&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nine places in our code use &lt;code&gt;request.base_url&lt;/code&gt;. All nine broke. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000",&lt;/span&gt;
     "--proxy-headers", "--forwarded-allow-ips", "*"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're running uvicorn behind any reverse proxy and generating URLs from &lt;code&gt;request.base_url&lt;/code&gt;, you need these flags. You won't catch this locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Tag-Based Access Control
&lt;/h2&gt;

&lt;p&gt;HAPI has no per-user access control. Our gateway appends &lt;code&gt;_tag&lt;/code&gt; parameters to scope searches by tenant/dataset.&lt;/p&gt;

&lt;p&gt;The gotcha: &lt;strong&gt;HAPI ignores &lt;code&gt;_tag&lt;/code&gt; on instance reads.&lt;/strong&gt; &lt;code&gt;GET /fhir/Patient/abc-123&lt;/code&gt; returns the resource regardless of tags. Our gateway does post-fetch verification — fetch the resource, check its tags, return 404 if they don't match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resource_tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;resource_tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowed_tags&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not ideal — the resource is fetched and discarded. But HAPI doesn't support tag filtering on instance reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Streaming Large Responses
&lt;/h2&gt;

&lt;p&gt;A FHIR &lt;code&gt;$everything&lt;/code&gt; can return megabytes. Don't buffer it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aiter_bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/fhir+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha: if HAPI returns 503, detect it &lt;em&gt;before&lt;/em&gt; starting the stream. Once you've started a &lt;code&gt;StreamingResponse&lt;/code&gt;, you can't change the status code.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Catch-All Route Ordering Trap
&lt;/h2&gt;

&lt;p&gt;Our FHIR proxy has a catch-all: &lt;code&gt;/fhir/{path:path}&lt;/code&gt;. This matches everything — including &lt;code&gt;/fhir/Patient/$export&lt;/code&gt; and &lt;code&gt;/fhir/DocumentReference/$docref&lt;/code&gt;, which have dedicated handlers.&lt;/p&gt;

&lt;p&gt;If the catch-all mounts first, those routes are unreachable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Order matters
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bulk_export_router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# most specific
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docref_router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fhir_router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# catch-all last
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds obvious. Costs an afternoon when you add a new router six months later and forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Gateway First
&lt;/h2&gt;

&lt;p&gt;If you're deploying a FHIR server to production, build the gateway layer before you build features on top. Auth, URL generation, access control, streaming, and route safety affect every endpoint you'll add later. The FHIR server stores data. The gateway decides who sees it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; handles the gateway — SMART auth, access control, streaming — so you can focus on your application. &lt;a href="https://mock.health/login" rel="noopener noreferrer"&gt;Try it free →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fhir</category>
      <category>healthtech</category>
      <category>python</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>The Contents of That Dumpster Are Private</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Mon, 06 Apr 2026 23:09:23 +0000</pubDate>
      <link>https://forem.com/mockhealth/the-contents-of-that-dumpster-are-private-4dok</link>
      <guid>https://forem.com/mockhealth/the-contents-of-that-dumpster-are-private-4dok</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foigjap1s4r08csr4fm29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foigjap1s4r08csr4fm29.png" alt=" " width="800" height="693"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nikhil Krishnan wrote &lt;a href="https://www.outofpocket.health/p/open-source-in-healthcare-is-an-opportunity" rel="noopener noreferrer"&gt;a great piece recently&lt;/a&gt; arguing that open source is healthcare's moment. The business models exist (Red Hat, GitLab). AI is lowering the contribution barrier. The ecosystem is ready.&lt;/p&gt;

&lt;p&gt;He's right. But there's a punchline missing from the open-standards conversation that anyone who's actually &lt;em&gt;built&lt;/em&gt; against healthcare APIs knows intimately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The standard exists. Nobody follows it the same way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FHIR (Fast Healthcare Interoperability Resources) is the anointed standard. ONC mandates it. CMS requires it for payer Patient Access APIs. Every major EHR vendor will tell you they support it. And technically, they do — the way a restaurant "supports" vegetarians by offering a side salad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same Patient, Different Reality
&lt;/h2&gt;

&lt;p&gt;Flexpa recently published a fascinating comparison: &lt;a href="https://www.flexpa.com/blog/my-health-data-across-two-apis" rel="noopener noreferrer"&gt;the same patient's health data pulled through two different API pathways&lt;/a&gt;. The results should make anyone building healthcare software nervous:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TEFCA path&lt;/strong&gt;: 192 FHIR resources, but only &lt;strong&gt;1 condition&lt;/strong&gt; — "pain in throat"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ONC g(10) path&lt;/strong&gt;: 166 resources, &lt;strong&gt;11 conditions&lt;/strong&gt; including rhinitis — but missed a documented cat dander allergy entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same patient. Same standard. Wildly different clinical picture. If you were building a care coordination app, which version of this patient would you trust?&lt;/p&gt;

&lt;p&gt;This isn't an edge case. It's the norm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spec Says One Thing. Production Says Another.
&lt;/h2&gt;

&lt;p&gt;The January 2026 deadline for USCDI v3 and US Core 6.1.0 was supposed to fix this. Certified health IT modules must now support the latest FHIR profiles with richer data elements. On paper, progress.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Major state Medicaid agencies&lt;/strong&gt; — Arizona, Colorado, Illinois, Indiana, Massachusetts, New York, Pennsylvania, Texas — remain non-compliant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No BCBS plan operated by HCSC&lt;/strong&gt; has successfully had a patient complete authorization and FHIR data retrieval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;53 payers&lt;/strong&gt; list refresh token support in their SMART configurations but &lt;em&gt;don't actually issue refresh tokens&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UnitedHealthcare&lt;/strong&gt; has nearly 100 patients who simply cannot access their own data through the mandated API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't obscure edge cases. These are the largest payers and state programs in the country.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Normalization Tax
&lt;/h2&gt;

&lt;p&gt;Even when the APIs work, the data that comes back requires heavy lifting before it's usable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zushealth.com/pipe-dreams-building-smarter-healthcare-data-networks-for-better-care/" rel="noopener noreferrer"&gt;Zus Health reports&lt;/a&gt; that pulling records from clinical networks yields about 200 documents per patient, mapping to over 1,000 raw FHIR resources. Conditions arrive as a grab bag of ICD-9, ICD-10, and SNOMED codes — sometimes all three for the same condition. Without normalization, deduplication, and enrichment, the volume is pure noise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.particlehealth.com/blog/ensuring-data-quality-part-2" rel="noopener noreferrer"&gt;Particle Health&lt;/a&gt; built an entirely proprietary CCDA-to-FHIR converter because open source libraries didn't meet their quality bar.&lt;/p&gt;

&lt;p&gt;This is the dirty secret: &lt;strong&gt;an entire category of healthcare infrastructure companies exists primarily to clean up the mess that "standardized" data exchange creates.&lt;/strong&gt; The standard is the floor, not the ceiling — and the floor has holes in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for Trash
&lt;/h2&gt;

&lt;p&gt;FHIR is a good standard. US Core is a good implementation guide. The ONC keeps tightening the spec — USCDI v3, US Core 6.1.0, SMART App Launch 2.0. But tighter specs only help if implementations actually conform. Right now, "FHIR-compliant" is a checkbox on a certification form, not a meaningful guarantee that the data you receive will be complete, consistent, or even parseable.&lt;/p&gt;

&lt;p&gt;So here's the engineering takeaway: &lt;strong&gt;stop hoping for clean data. Plan for trash.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every application that consumes FHIR from external systems needs a validation layer that isn't optional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile validation on ingest.&lt;/strong&gt; Every resource that crosses your system boundary gets validated against the US Core profile you expect. Not just schema validation — profile-level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness checks per data class.&lt;/strong&gt; USCDI defines 22 data classes. When you pull a patient's record, how many are actually populated? Track and surface that distinction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated regression testing against real-world variance.&lt;/strong&gt; Your integration tests shouldn't run against a single pristine FHIR server. They should run against data that looks like what production actually sends — missing fields, mixed code systems, DSTU2 holdovers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The companies actually moving the needle in interoperability — Flexpa, Zus, Particle, Onyx — all learned this the hard way. They built proprietary normalization, deduplication, and validation pipelines because the alternative was shipping broken products. The standard isn't bad. Conformance is just unreliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dumpster Metaphor, Extended
&lt;/h2&gt;

&lt;p&gt;The meme says the contents of the dumpster are private. That's the HIPAA joke, and it's funny. But the deeper joke is:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Even if you get permission to open the dumpster, what's inside is still trash.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Don't wait for the spec to get stricter, or for ONC enforcement to get teeth, or for vendors to suddenly care about data quality. Build like you know the dumpster is full of trash — because it is — and validate everything that comes out of it.&lt;/p&gt;

&lt;p&gt;Which means your test data needs to look like what production actually sends — not pristine sandbox patients with perfect coding. If your integration tests pass against clean data and break against the dumpster, your tests are lying to you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; generates FHIR data with the clinical density and variance you'll see in the real world. &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;Free tier, no sales call →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fhir</category>
      <category>healthtech</category>
      <category>api</category>
      <category>interoperability</category>
    </item>
    <item>
      <title>How to Make Claude Write Valid Synthea Modules</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Fri, 03 Apr 2026 22:24:49 +0000</pubDate>
      <link>https://forem.com/mockhealth/how-to-make-claude-write-valid-synthea-modules-3adc</link>
      <guid>https://forem.com/mockhealth/how-to-make-claude-write-valid-synthea-modules-3adc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F23xh3b1u7z1dkpygzvyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F23xh3b1u7z1dkpygzvyw.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/synthetichealth/synthea" rel="noopener noreferrer"&gt;Synthea&lt;/a&gt; has 85 disease modules. Each one is a JSON state machine that generates encounters, conditions, labs, medications, and procedures for a specific disease. If you need a condition Synthea doesn't cover — celiac disease, migraine, GERD, whatever — you author a new module.&lt;/p&gt;

&lt;p&gt;The module format is learnable. The hard part is the medical codes.&lt;/p&gt;

&lt;p&gt;Every module embeds SNOMED codes for conditions, LOINC codes for labs, and RxNorm codes for medications. Ask an LLM to write a celiac disease module and it'll generate SNOMED code &lt;code&gt;396331005&lt;/code&gt;. That's correct. Ask it for a duodenal biopsy and it might generate &lt;code&gt;12866006&lt;/code&gt;. Looks right. Validates as a real SNOMED code. It's actually pneumococcal vaccination.&lt;/p&gt;

&lt;p&gt;You can't tell a valid code from a hallucinated one by looking at it. The only way to know is to check it against a terminology server. This post teaches that workflow, and we published a &lt;a href="https://github.com/mock-health/samples/tree/main/synthea-module-skill" rel="noopener noreferrer"&gt;Claude Code skill&lt;/a&gt; that automates it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You'd Want Custom Modules
&lt;/h2&gt;

&lt;p&gt;We generated 10,000 patients from a fresh Synthea clone and compared against CDC benchmarks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0njadgxk9rf1snxzna2m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0njadgxk9rf1snxzna2m.png" alt="Synthea vs CDC condition prevalence" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coronary heart disease: 0%. Alzheimer's: 0%. Entire disease categories missing from 10,000 patients. Each module runs independently, so conditions don't interact the way real diseases do. We wrote about this in depth in &lt;a href="https://mock.health/blog/how-we-validate-synthetic-data" rel="noopener noreferrer"&gt;how we validate synthetic data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6471x715cy06eirnof5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6471x715cy06eirnof5.png" alt="Conditions per patient by age bracket" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An average 80+ year-old Synthea patient has 74 active conditions. The top 1% of real Medicare patients have about 8. Most are social determinants and administrative codes, not diseases.&lt;/p&gt;

&lt;p&gt;If your use case needs a disease that Synthea's 85 modules don't cover, you're authoring a module.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Module Format in 60 Seconds
&lt;/h2&gt;

&lt;p&gt;A Synthea module is a JSON file with a &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;states&lt;/code&gt; object, and a &lt;code&gt;gmf_version&lt;/code&gt;. Each state has a type and a transition. Here's the smallest useful module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Condition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"states"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Initial"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Initial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"distributed_transition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"transition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Onset"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"transition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Terminal"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Onset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ConditionOnset"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SNOMED-CT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"??????"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"??????"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"direct_transition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Terminal"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Terminal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Terminal"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gmf_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;??????&lt;/code&gt; is the problem. What SNOMED code goes there?&lt;/p&gt;

&lt;p&gt;State types you'll use: &lt;code&gt;Initial&lt;/code&gt;, &lt;code&gt;Terminal&lt;/code&gt;, &lt;code&gt;Encounter&lt;/code&gt;/&lt;code&gt;EncounterEnd&lt;/code&gt;, &lt;code&gt;ConditionOnset&lt;/code&gt;/&lt;code&gt;ConditionEnd&lt;/code&gt;, &lt;code&gt;MedicationOrder&lt;/code&gt;/&lt;code&gt;MedicationEnd&lt;/code&gt;, &lt;code&gt;Observation&lt;/code&gt;, &lt;code&gt;Procedure&lt;/code&gt;, &lt;code&gt;Guard&lt;/code&gt;, &lt;code&gt;Delay&lt;/code&gt;, &lt;code&gt;SetAttribute&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Transitions: &lt;code&gt;direct_transition&lt;/code&gt; (always), &lt;code&gt;conditional_transition&lt;/code&gt; (if-then), &lt;code&gt;distributed_transition&lt;/code&gt; (probabilistic), &lt;code&gt;complex_transition&lt;/code&gt; (conditions + probabilities).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code Problem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;What it codes&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SNOMED-CT&lt;/td&gt;
&lt;td&gt;Conditions, procedures, findings&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;396331005&lt;/code&gt; = Celiac disease&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOINC&lt;/td&gt;
&lt;td&gt;Lab results, vital signs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;31017-7&lt;/code&gt; = tTG IgA antibody&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RxNorm&lt;/td&gt;
&lt;td&gt;Medications&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;310325&lt;/code&gt; = Ferrous sulfate 325mg&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;LLMs pattern-match these codes from training data. They don't look them up. For common conditions, usually right. For anything less common, the LLM generates a plausible number that might not exist.&lt;/p&gt;

&lt;p&gt;The fix: &lt;a href="https://tx.fhir.org" rel="noopener noreferrer"&gt;tx.fhir.org&lt;/a&gt;, a free public FHIR terminology server. No account needed. One curl call validates any code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://tx.fhir.org/r4/CodeSystem/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;validate-code?&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
system=http://snomed.info/sct&amp;amp;code=396331005"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.parameter[] | select(.name=="result" or .name=="display")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueBoolean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Coeliac disease"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to &lt;em&gt;find&lt;/em&gt; the right code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://tx.fhir.org/r4/ValueSet/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;expand?&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
url=http://snomed.info/sct?fhir_vs&amp;amp;filter=celiac+disease&amp;amp;count=5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.expansion.contains[] | {code, display}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what separates a working module from one that generates corrupt FHIR.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Skill
&lt;/h2&gt;

&lt;p&gt;We published a Claude Code skill that automates this workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nb"&gt;install &lt;/span&gt;github:mock-health/samples/synthea-module-skill
claude &lt;span class="s2"&gt;"/synthea create a celiac disease module"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill follows six steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check existing modules&lt;/strong&gt; — avoids duplicating what's already there&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research the condition&lt;/strong&gt; — prevalence, diagnostic criteria, treatment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look up every code&lt;/strong&gt; — validates against tx.fhir.org before writing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate module JSON&lt;/strong&gt; — following Synthea's exact schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build and test&lt;/strong&gt; — &lt;code&gt;./gradlew build -x test&lt;/code&gt; + &lt;code&gt;./run_synthea -m &amp;lt;name&amp;gt; -p 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect output&lt;/strong&gt; — checks the generated FHIR bundle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://github.com/mock-health/samples/tree/main/synthea-module-skill/SKILL.md" rel="noopener noreferrer"&gt;SKILL.md&lt;/a&gt; contains the full module schema reference, code system mappings, grounding rules, and common pitfalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Example: Celiac Disease
&lt;/h2&gt;

&lt;p&gt;Every code validated against tx.fhir.org:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Display&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Celiac disease&lt;/td&gt;
&lt;td&gt;SNOMED-CT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;396331005&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coeliac disease&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EGD&lt;/td&gt;
&lt;td&gt;SNOMED-CT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;76009000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Esophagogastroduodenoscopy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duodenal biopsy&lt;/td&gt;
&lt;td&gt;SNOMED-CT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;235261009&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Biopsy of duodenum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gluten free diet&lt;/td&gt;
&lt;td&gt;SNOMED-CT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;160671006&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gluten free diet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iron deficiency anemia&lt;/td&gt;
&lt;td&gt;SNOMED-CT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;87522002&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Iron deficiency anemia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tTG IgA antibody&lt;/td&gt;
&lt;td&gt;LOINC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;31017-7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tissue transglutaminase IgA Ab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ferritin&lt;/td&gt;
&lt;td&gt;LOINC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2276-4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ferritin [Mass/volume] in Serum or Plasma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ferrous sulfate&lt;/td&gt;
&lt;td&gt;RxNorm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;310325&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ferrous sulfate 325 MG Oral Tablet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial → Age_Guard (wait until age 2)
  → Prevalence_Check (monthly, age-stratified probability)
    → Symptom_Onset → Diagnostic_Encounter
      → tTG IgA test → Referral (2-6 weeks)
        → Endoscopy + Duodenal biopsy
          → Celiac_Diagnosis → Gluten-free diet
            → 50%: Iron deficiency → Ferrous sulfate
              → Annual monitoring (tTG IgA + ferritin)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module uses &lt;code&gt;complex_transition&lt;/code&gt; for age-stratified onset — childhood and ages 30-50 get different probabilities. The &lt;a href="https://github.com/mock-health/samples/tree/main/synthea-module-skill/celiac_disease.json" rel="noopener noreferrer"&gt;full module JSON&lt;/a&gt; is about 200 lines. Drop it into &lt;code&gt;synthea/src/main/resources/modules/&lt;/code&gt; and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;synthea
./gradlew build &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
./run_synthea &lt;span class="nt"&gt;-m&lt;/span&gt; celiac_disease &lt;span class="nt"&gt;-p&lt;/span&gt; 10 &lt;span class="nt"&gt;-s&lt;/span&gt; 42
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.entry[].resource.resourceType'&lt;/span&gt; output/fhir/&lt;span class="k"&gt;*&lt;/span&gt;.json | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  For Deeper Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/fhir&lt;/code&gt; Claude Code skill&lt;/strong&gt; — FHIR R4/R5, IGs, FSH, SMART on FHIR, validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://inferno.healthit.gov/" rel="noopener noreferrer"&gt;Inferno&lt;/a&gt;&lt;/strong&gt; — ONC FHIR server compliance testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://synthetichealth.github.io/module-builder/" rel="noopener noreferrer"&gt;Synthea Module Builder&lt;/a&gt;&lt;/strong&gt; — GUI for visual module authoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tx.fhir.org" rel="noopener noreferrer"&gt;tx.fhir.org&lt;/a&gt;&lt;/strong&gt; — public FHIR terminology server (free, no account)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;The module format is learnable. The vocabulary problem is solvable. Ground your codes, and don't trust any medical code an LLM generates from memory — including ours. We built &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; because we hit the limits of module-level fixes. Population-level realism requires &lt;a href="https://mock.health/blog/how-we-validate-synthetic-data" rel="noopener noreferrer"&gt;a different architecture entirely&lt;/a&gt;. &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;Free tier, API key in 60 seconds →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>healthtech</category>
      <category>fhir</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build a SMART on FHIR App in 30 Minutes</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Thu, 26 Mar 2026 20:55:05 +0000</pubDate>
      <link>https://forem.com/mockhealth/build-a-smart-on-fhir-app-in-30-minutes-3g39</link>
      <guid>https://forem.com/mockhealth/build-a-smart-on-fhir-app-in-30-minutes-3g39</guid>
      <description>&lt;p&gt;SMART on FHIR is how apps talk to electronic health records. It's OAuth 2.0 with a few healthcare-specific conventions: a discovery document that tells your app where to authenticate, scopes that map to FHIR resource types, and a launch context that tells you which patient you're looking at.&lt;/p&gt;

&lt;p&gt;This tutorial follows &lt;a href="https://hl7.org/fhir/smart-app-launch/STU2.1/" rel="noopener noreferrer"&gt;SMART App Launch STU2.1&lt;/a&gt; — the current published standard. If you've built an OAuth login flow before, you already understand 80% of it. The other 20% is what this covers.&lt;/p&gt;

&lt;p&gt;We're going to build a standalone SMART app that authenticates against a FHIR server, fetches a patient's vital signs, and renders interactive charts — heart rate, blood pressure, SpO2, temperature, weight, and BMI. The whole thing fits in a single &lt;code&gt;index.html&lt;/code&gt; file. No React, no bundler, no npm install. Just a text editor and a browser.&lt;/p&gt;

&lt;p&gt;By the end you'll have a working app and a mental model for how every SMART on FHIR app works under the hood — whether it's a one-page demo or a production clinical decision support tool launching inside Epic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A mock.health account.&lt;/strong&gt; Go to &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; and sign up. It's free. This gives you a FHIR R4 server with synthetic patients that actually have realistic clinical data — vital signs, conditions, medications, imaging studies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A registered SMART app.&lt;/strong&gt; In the mock.health dashboard, go to &lt;strong&gt;Sandbox → Register App&lt;/strong&gt; and create a new app with these settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App Name:&lt;/strong&gt; Vital Signs Chart (or whatever you want)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URI:&lt;/strong&gt; &lt;code&gt;http://localhost:8080/smart-on-fhir-vital-signs/index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes:&lt;/strong&gt; &lt;code&gt;launch/patient&lt;/code&gt;, &lt;code&gt;patient/Patient.rs&lt;/code&gt;, &lt;code&gt;patient/Observation.rs&lt;/code&gt;, &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;fhirUser&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy the &lt;strong&gt;Client ID&lt;/strong&gt; — you'll need it in a minute.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on scope syntax.&lt;/strong&gt; SMART App Launch STU2 introduced a new scope format. The v1 format &lt;code&gt;patient/Patient.read&lt;/code&gt; became &lt;code&gt;patient/Patient.rs&lt;/code&gt; in v2, where the suffix letters map to individual operations: &lt;strong&gt;c&lt;/strong&gt;reate, &lt;strong&gt;r&lt;/strong&gt;ead, &lt;strong&gt;u&lt;/strong&gt;pdate, &lt;strong&gt;d&lt;/strong&gt;elete, &lt;strong&gt;s&lt;/strong&gt;earch. So &lt;code&gt;.rs&lt;/code&gt; means read + search, and v1's &lt;code&gt;.write&lt;/code&gt; maps to v2's &lt;code&gt;.cud&lt;/code&gt;. Servers advertise which format they support via &lt;code&gt;permission-v1&lt;/code&gt; and &lt;code&gt;permission-v2&lt;/code&gt; in their capabilities. Most production EHRs accept both formats today. This tutorial uses v2 (&lt;code&gt;*.rs&lt;/code&gt;), but v1 (&lt;code&gt;*.read&lt;/code&gt;) will also work with mock.health.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The redirect URI matters. It must exactly match the URL where your app is running. When the authorization server sends the user back to your app after login, it appends an authorization code to this URL. If it doesn't match what you registered, the server rejects the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Discover the SMART Endpoints
&lt;/h2&gt;

&lt;p&gt;Every SMART-enabled FHIR server publishes a discovery document at &lt;code&gt;/.well-known/smart-configuration&lt;/code&gt;. This tells your app where to send the user for authorization and where to exchange codes for tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.mock.health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/fhir/.well-known/smart-configuration`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// config.authorization_endpoint → where to redirect the user&lt;/span&gt;
&lt;span class="c1"&gt;// config.token_endpoint         → where to exchange the code for a token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.mock.health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.mock.health/smart/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.mock.health/smart/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grant_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code_challenge_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"launch/patient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"patient/Patient.rs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"patient/Observation.rs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fhirUser"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"launch-standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client-public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"context-standalone-patient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission-v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sso-openid-connect"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why discovery instead of hardcoding? Because every FHIR server puts these endpoints at different paths. Epic uses &lt;code&gt;/oauth2/authorize&lt;/code&gt;. Oracle Health (Cerner) uses &lt;code&gt;/tenants/{tenant}/protocols/oauth2/profiles/smart-v1/personas/patient/authorize&lt;/code&gt;. The discovery document means your app works against any conformant server without code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Generate a PKCE Challenge
&lt;/h2&gt;

&lt;p&gt;PKCE (Proof Key for Code Exchange, pronounced "pixie") prevents authorization code interception attacks. Your app generates a random secret (the &lt;em&gt;verifier&lt;/em&gt;), hashes it (the &lt;em&gt;challenge&lt;/em&gt;), and sends only the hash to the authorization server. When you exchange the code for a token later, you prove you're the same app by sending the original verifier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateCodeVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;base64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computeCodeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;base64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;base64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/=+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the verifier in &lt;code&gt;sessionStorage&lt;/code&gt; — you'll need it after the redirect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateCodeVerifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;computeCodeChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pkce_verifier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Redirect to Authorize
&lt;/h2&gt;

&lt;p&gt;Build the authorization URL and redirect the user's browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;launch/patient patient/Patient.rs patient/Observation.rs openid fhirUser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/fhir`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code_challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code_challenge_method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization_endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each parameter does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response_type: 'code'&lt;/code&gt;&lt;/strong&gt; — We want an authorization code. Always &lt;code&gt;code&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;client_id&lt;/code&gt;&lt;/strong&gt; — Identifies your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redirect_uri&lt;/code&gt;&lt;/strong&gt; — Where to send the user back. Must exactly match what you registered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scope&lt;/code&gt;&lt;/strong&gt; — What data you're requesting. &lt;code&gt;launch/patient&lt;/code&gt; asks for a patient context. &lt;code&gt;patient/Observation.rs&lt;/code&gt; asks to read and search Observations scoped to that patient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;state&lt;/code&gt;&lt;/strong&gt; — A random string to prevent CSRF attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/strong&gt; — The FHIR endpoint this token should work with. Prevents token replay across servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;code_challenge&lt;/code&gt;&lt;/strong&gt; — The PKCE challenge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this redirect, the user sees a consent screen. When they approve, the server redirects back to your &lt;code&gt;redirect_uri&lt;/code&gt; with &lt;code&gt;?code=...&amp;amp;state=...&lt;/code&gt; appended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Handle the Callback and Exchange the Code
&lt;/h2&gt;

&lt;p&gt;When your page loads, check if there's a &lt;code&gt;code&lt;/code&gt; parameter in the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;State mismatch — possible CSRF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pkce_verifier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code_verifier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenEndpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token response includes everything you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJ0eXAiOiJKV1QiLCJhbGci..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launch/patient patient/Patient.rs patient/Observation.rs openid fhirUser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"patient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-e5f6-7890-abcd-ef1234567890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJhbGciOiJSUzI1NiJ9..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two SMART-specific fields: &lt;strong&gt;&lt;code&gt;patient&lt;/code&gt;&lt;/strong&gt; is the launch context — the ID of the patient the user selected. &lt;strong&gt;&lt;code&gt;id_token&lt;/code&gt;&lt;/strong&gt; contains the authenticated user's identity via the &lt;code&gt;fhirUser&lt;/code&gt; claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Fetch Vital Signs
&lt;/h2&gt;

&lt;p&gt;Now you have a bearer token and a patient ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/fhir+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;patient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/fhir/Patient/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/fhir/Observation?patient=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;category=vital-signs&amp;amp;_sort=-date&amp;amp;_count=200`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Observation has a LOINC code that identifies what it measures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vital Sign&lt;/th&gt;
&lt;th&gt;LOINC Code&lt;/th&gt;
&lt;th&gt;Value Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heart Rate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8867-4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;valueQuantity.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blood Pressure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;85354-9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;component[].valueQuantity.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temperature&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8310-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;valueQuantity.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpO2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2708-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;valueQuantity.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weight&lt;/td&gt;
&lt;td&gt;&lt;code&gt;29463-7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;valueQuantity.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BMI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;39156-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;valueQuantity.value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Blood pressure is the odd one out. It's a &lt;em&gt;panel&lt;/em&gt; — the Observation itself has LOINC code &lt;code&gt;85354-9&lt;/code&gt;, but the actual systolic and diastolic values live in &lt;code&gt;component&lt;/code&gt; entries with their own LOINC codes (&lt;code&gt;8480-6&lt;/code&gt; for systolic, &lt;code&gt;8462-4&lt;/code&gt; for diastolic). Every FHIR developer hits this for the first time and wonders why it's nested. Welcome to the club.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Render the Charts
&lt;/h2&gt;

&lt;p&gt;We use &lt;a href="https://www.chartjs.org/" rel="noopener noreferrer"&gt;Chart.js&lt;/a&gt; — one CDN script tag, no build step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/chart.js@4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Group observations by type, sort by date, render a line chart for each. For blood pressure, render two datasets on the same chart — systolic and diastolic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete App
&lt;/h2&gt;

&lt;p&gt;The full working app is ~475 lines in a single &lt;code&gt;index.html&lt;/code&gt;. Clone it and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mock-health/samples.git
&lt;span class="nb"&gt;cd &lt;/span&gt;samples
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080

&lt;span class="c"&gt;# Open http://localhost:8080/smart-on-fhir-vital-signs/index.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter your Client ID, click &lt;strong&gt;Connect&lt;/strong&gt;, approve access, and you'll see your patient's vital signs charted out.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mock-health/samples/tree/main/smart-on-fhir-vital-signs" rel="noopener noreferrer"&gt;full source is on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes in Production EHRs
&lt;/h2&gt;

&lt;p&gt;This tutorial uses mock.health, which implements the SMART spec faithfully. Production EHRs are different:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App registration takes weeks, not seconds.&lt;/strong&gt; Epic's App Orchard requires a formal review process. Budget 2–8 weeks for approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scopes vary.&lt;/strong&gt; Some EHRs still only accept v1 syntax (&lt;code&gt;patient/Observation.read&lt;/code&gt;). Check the server's &lt;code&gt;.well-known/smart-configuration&lt;/code&gt; capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery endpoints aren't always where you expect.&lt;/strong&gt; Some older servers don't support &lt;code&gt;.well-known/smart-configuration&lt;/code&gt;. Fall back to the &lt;code&gt;CapabilityStatement&lt;/code&gt; (&lt;code&gt;GET /metadata&lt;/code&gt;) and extract OAuth URLs from &lt;code&gt;rest[0].security.extension&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens expire and refresh tokens matter.&lt;/strong&gt; In production, request &lt;code&gt;offline_access&lt;/code&gt; scope and implement refresh token rotation.&lt;/p&gt;

&lt;p&gt;None of this changes the fundamental flow. Discovery → PKCE → authorize → callback → token → API calls. Same everywhere. The operational details differ.&lt;/p&gt;




&lt;p&gt;Sign up at &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; — free tier, no sales call. The hardest part of building a SMART on FHIR app is getting access to a server that has realistic data and implements the spec correctly. Now you have one.&lt;/p&gt;

</description>
      <category>fhir</category>
      <category>healthtech</category>
      <category>tutorial</category>
      <category>oauth</category>
    </item>
    <item>
      <title>The FHIR Sandbox Problem: Why Open Epic Won't Get You to Your First Customer</title>
      <dc:creator>mock health</dc:creator>
      <pubDate>Sun, 22 Mar 2026 21:05:56 +0000</pubDate>
      <link>https://forem.com/mockhealth/the-fhir-sandbox-problem-why-open-epic-wont-get-you-to-your-first-customer-4nea</link>
      <guid>https://forem.com/mockhealth/the-fhir-sandbox-problem-why-open-epic-wont-get-you-to-your-first-customer-4nea</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hzlczkg4cts8pmhk6ms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hzlczkg4cts8pmhk6ms.png" alt=" " width="654" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You're three months into building a FHIR integration. You've got OAuth working, you can pull Patient resources, your UI renders demographics cleanly. Time to show it to a potential customer.&lt;/p&gt;

&lt;p&gt;You open the sandbox patient and see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Patient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"erXuFYUfucBZaryVksYEcMg3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usual"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test Cancer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"family"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cancer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"given"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Test"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"birthDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1971-08-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"female"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"123 Main St."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Madison"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"postalCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"53703"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The patient's name is "Test Cancer." The address is 123 Main St. There are no emergency contacts, no marital status, no communication preferences. You scroll through the rest of the sandbox patients. "Derrick Lin." "Jason Argonaut." A handful of others, all equally sparse.&lt;/p&gt;

&lt;p&gt;This is Open Epic's sandbox. And it's the starting point for nearly every FHIR startup in the US.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Open Epic Actually Gives You
&lt;/h2&gt;

&lt;p&gt;Open Epic provides a shared FHIR R4 sandbox with eight named test patients (Camila Lopez, Derrick Lin, Desiree Powell, Elijah Davis, Linda Ross, Olivia Roberts, Warren McGinnis, and Jason Argonaut). These patients exist to validate API connectivity — to confirm that your OAuth flow works, that you can parse a Bundle, that your FHIR client handles pagination. They were never designed to represent what real patient data looks like.&lt;/p&gt;

&lt;p&gt;Here's what the sandbox gives you versus what you need to demo a real clinical application:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Open Epic Sandbox&lt;/th&gt;
&lt;th&gt;Production Demo Needs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Patient demographics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Name, DOB, gender, address&lt;/td&gt;
&lt;td&gt;Race, ethnicity, language, multiple addresses, contacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conditions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-3 active conditions&lt;/td&gt;
&lt;td&gt;5-15 conditions with onset dates, comorbidity patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Medications&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sparse, often missing&lt;/td&gt;
&lt;td&gt;Active + historical, with dosage and frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lab observations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Few, no trends&lt;/td&gt;
&lt;td&gt;Longitudinal panels (HbA1c over 3 years, lipid trends)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Imaging studies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;ImagingStudy + DiagnosticReport with narrative text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clinical notes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None or minimal&lt;/td&gt;
&lt;td&gt;Discharge summaries, progress notes, radiology reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Encounters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-3 encounters&lt;/td&gt;
&lt;td&gt;Multi-year history across ambulatory, inpatient, ED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vital signs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sparse&lt;/td&gt;
&lt;td&gt;Trending data (BP, weight, heart rate over time)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sandbox is optimized for certification. It proves your app can authenticate and read FHIR resources. It says nothing about whether your app can handle a real patient record.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Alternative Landscape Is Shrinking
&lt;/h3&gt;

&lt;p&gt;Open Epic isn't the only sandbox, but the options are thinner than you'd think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logica Health&lt;/strong&gt; (formerly HSPC) ran a well-regarded public sandbox for years. It was retired on October 31, 2024. If your integration documentation still points there, those links are dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SMART Health IT&lt;/strong&gt; maintains a sandbox with about 100 Synthea-generated patients. The data is structurally valid but clinically shallow — default Synthea output without enrichment. (If you've used it, you know the feeling: the FHIR parses fine, but the patient has three conditions and no meds.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Individual health systems&lt;/strong&gt; sometimes provide sandbox access, but getting it requires a signed BAA, a security review, and months of procurement. That's not a sandbox. That's a sales cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap Between Sandbox and Production
&lt;/h2&gt;

&lt;p&gt;Missing fields are the obvious problem. The deeper problem is missing &lt;em&gt;clinical coherence&lt;/em&gt; — real patient data tells a story, and sandbox data doesn't have one.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Comorbidities
&lt;/h3&gt;

&lt;p&gt;In the real world, Type 2 Diabetes doesn't travel alone. It shows up with hypertension, hyperlipidemia, chronic kidney disease, and obesity. A 62-year-old diabetic in a production EHR typically has 8-15 active conditions, most of them clinically related.&lt;/p&gt;

&lt;p&gt;Open Epic's test patients have conditions, but they're isolated. You won't find a patient whose diabetes diagnosis is followed by quarterly HbA1c labs trending from 7.2 to 8.1 over 18 months, with a metformin prescription added at diagnosis and a GLP-1 agonist added when the HbA1c crossed 8.0. That's what your app needs to demo against.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Imaging, No Clinical Narrative
&lt;/h3&gt;

&lt;p&gt;Search for ImagingStudy resources in the Open Epic sandbox. Empty bundles. No DiagnosticReports with radiology narrative. No discharge summaries, no progress notes. Production EHRs have &lt;code&gt;presentedForm.data&lt;/code&gt; fields with base64-encoded report text — the full narrative a radiologist dictated. Sandboxes have "FINDINGS: Normal. IMPRESSION: Normal." or nothing at all.&lt;/p&gt;

&lt;p&gt;If you're building anything that touches imaging, clinical NLP, or document summarization — you have zero test data to work with.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Longitudinal History
&lt;/h3&gt;

&lt;p&gt;Real patients have years of history. A production Patient sits at the center of hundreds of linked resources — encounters spanning a decade, medication changes over time, lab trends that tell a clinical story.&lt;/p&gt;

&lt;p&gt;Sandbox patients have one or two encounters. You can't demonstrate a timeline view, trend analysis, or care gap detection when the patient's entire history fits in a single API response.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Actually Get: Epic Sandbox vs. Production
&lt;/h3&gt;

&lt;p&gt;To make this concrete, here's a real query against the Epic on FHIR sandbox for Camila Lopez — the "Test Cancer" patient from the opening of this post — filtering for laboratory observations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/Observation&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
?patient=erXuFYUfucBZaryVksYEcMg3&amp;amp;category=laboratory"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/fhir+json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.total, [.entry[].resource |
    {code: .code.text, date: .effectiveDateTime,
     value: (.valueQuantity.value | tostring) + " " + .valueQuantity.unit}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A representative sandbox response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c/Hemoglobin.total in Blood"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2019-06-10T15:38:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.6 %"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glucose [Mass/volume] in Blood"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2019-06-10T15:38:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"138 mg/dL"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two observations. Both from the same date. No prior values, no subsequent values. If you're building a diabetes management dashboard, a trend chart, or a care gap detector — this tells you nothing.&lt;/p&gt;

&lt;p&gt;Now compare that to what a production EHR returns — or what &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; generates — for a 62-year-old with Type 2 Diabetes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6.8 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-04-22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.1 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-07-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.4 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-10-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.9 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8.1 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-04-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.6 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-07-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.3 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-10-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.1 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.0 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hemoglobin A1c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-04-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6.9 %"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glucose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-01-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"128 mg/dL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glucose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-07-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"156 mg/dL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glucose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"172 mg/dL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glucose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-07-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"145 mg/dL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glucose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-04-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"132 mg/dL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen observations spanning two and a half years. You can see the HbA1c climbing from 6.8 to 8.1 — the point at which a clinician would escalate medication — then trending back down after treatment adjustment. The glucose values correlate. There's a clinical story in this data.&lt;/p&gt;

&lt;p&gt;The sandbox has the right resource types. The data inside them is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Realistic" Means for a FHIR Integration
&lt;/h2&gt;

&lt;p&gt;"Realistic" has a specific definition. It's in the spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  US Core Profiles
&lt;/h3&gt;

&lt;p&gt;&lt;a href="http://hl7.org/fhir/us/core/" rel="noopener noreferrer"&gt;US Core&lt;/a&gt; is the implementation guide that specifies how FHIR resources should be structured in the US healthcare system. Every EHR certified under ONC's Health IT Certification Program must conform to US Core profiles.&lt;/p&gt;

&lt;p&gt;US Core doesn't just say "include a Patient resource." It specifies which fields are &lt;strong&gt;required&lt;/strong&gt; (must be present), which are &lt;strong&gt;must-support&lt;/strong&gt; (must be handled if present), and which extensions are standard. A US Core Patient requires &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;gender&lt;/code&gt;, and &lt;code&gt;identifier&lt;/code&gt;. It must-supports &lt;code&gt;race&lt;/code&gt;, &lt;code&gt;ethnicity&lt;/code&gt;, &lt;code&gt;birthDate&lt;/code&gt;, and &lt;code&gt;communication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your test data doesn't include must-support fields, you can't verify that your app handles them correctly. And if your demo doesn't show them, your prospect — who looks at US Core-conformant data all day — will notice.&lt;/p&gt;

&lt;h3&gt;
  
  
  USCDI Data Classes
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://www.healthit.gov/isa/united-states-core-data-interoperability-uscdi" rel="noopener noreferrer"&gt;United States Core Data for Interoperability&lt;/a&gt; (USCDI) defines the minimum data classes that certified health IT must support. USCDI v3, mandatory as of January 2026, includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patient demographics (including sexual orientation and gender identity)&lt;/li&gt;
&lt;li&gt;Allergies and intolerances&lt;/li&gt;
&lt;li&gt;Medications (active and historical)&lt;/li&gt;
&lt;li&gt;Problems (conditions)&lt;/li&gt;
&lt;li&gt;Procedures&lt;/li&gt;
&lt;li&gt;Laboratory results&lt;/li&gt;
&lt;li&gt;Vital signs&lt;/li&gt;
&lt;li&gt;Clinical notes (discharge summaries, progress notes, consultation notes)&lt;/li&gt;
&lt;li&gt;Diagnostic imaging (reports and studies)&lt;/li&gt;
&lt;li&gt;Health insurance information&lt;/li&gt;
&lt;li&gt;Clinical tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your test data should cover all of these. The sandbox covers maybe four.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correlated Clinical Data
&lt;/h3&gt;

&lt;p&gt;The hardest gap to fill is missing &lt;em&gt;correlations&lt;/em&gt;. In real clinical data, conditions, medications, labs, and encounters are causally connected. A diabetes diagnosis triggers HbA1c monitoring every 3-6 months. An elevated creatinine prompts a nephrology referral. An abnormal chest X-ray leads to a CT follow-up.&lt;/p&gt;

&lt;p&gt;Standard synthetic data generators don't model these relationships. A &lt;a href="https://doi.org/10.1186/s12911-019-0793-0" rel="noopener noreferrer"&gt;2019 validation study&lt;/a&gt; of Synthea in &lt;em&gt;BMC Medical Informatics and Decision Making&lt;/em&gt; found that Synthea-generated populations showed 0% blood pressure control rates compared to 69.7% in real-world data. The resources parse. The population statistics are wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;You can query mock.health's FHIR API directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.mock.health/fhir/Patient?_count&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/fhir+json"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.entry[0].resource | {
    name: .name[0].text,
    birthDate,
    gender,
    race: .extension[] | select(.url | contains("race")) | .extension[0].valueCoding.display,
    ethnicity: .extension[] | select(.url | contains("ethnicity")) | .extension[0].valueCoding.display
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a radiology report with actual clinical narrative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.mock.health/fhir/DiagnosticReport?category=RAD&amp;amp;_count=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/fhir+json"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
    .entry[0].resource.presentedForm[0].data'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;base64 -d&lt;/code&gt; decodes the report text. You'll see findings, impressions, and clinical language — not "Normal. Normal. Normal."&lt;/p&gt;

&lt;p&gt;If you've already built a SMART on FHIR app, you can point it at &lt;code&gt;https://api.mock.health/fhir&lt;/code&gt; and get realistic clinical data through the same OAuth flow you'll use in production.&lt;/p&gt;

&lt;p&gt;Sign up at &lt;a href="https://mock.health" rel="noopener noreferrer"&gt;mock.health&lt;/a&gt; — free tier, no sales call.&lt;/p&gt;

</description>
      <category>healthcare</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
