Every healthcare app hits the same fork early: invent your own database schema, or model your clinical data as FHIR. The default instinct — "I'll just make a notes table and a patients table" — feels faster. It usually isn't. Here's how to make the call deliberately instead of by accident.
Should you build your healthcare app on FHIR?
Yes, in almost every case. If your app stores clinical data — patients, notes, vitals, assessments, diagnoses — model it as FHIR R4 from day one. You get interoperability and portability, and you avoid rebuilding a worse version of the FHIR resource catalog yourself. For agents, FHIR is the substrate, not the finished interface: the model still needs query-aware clinical context above the raw resource graph. Skip FHIR only for pure-consumer wellness tools that never touch a clinical system.
The custom-schema trap: you'll rebuild FHIR, badly
The seductive shortcut is to design your own tables. It feels lighter. But clinical data is deceptively deep, and you discover the depth one painful migration at a time:
- A "note" isn't a text column. It has an author, a status (draft → signed → amended), an encounter it belongs to, a patient, a type, and a timestamp that is not the row's
created_at. - A blood-pressure reading is two numbers, two units, a body site, a method, a "who measured it," and a coded concept — not
bp_systolic INT. - A diagnosis needs a coding system, a code, a clinical status, a verification status, and an onset that might be a date, an age, or "childhood."
- An allergy has a criticality, a reaction, a severity, and a "refuted vs. confirmed" state your
boolean has_allergycan't hold.
Each of these is a FHIR resource that a standards committee already argued about for years. When you roll your own, you re-litigate every one of those decisions under deadline pressure — and you get them subtly wrong. Six months in, your "simple" schema has accreted a status enum, a versioning hack, a coded-value side table, and a soft-delete column, and you've built FHIR, badly, without the ecosystem. The standard exists precisely so you don't have to.
FHIR is the Series-A expectation
If you plan to raise, get acquired, or sell into a health system, your data model becomes due-diligence material. The questions are predictable: "Can you export a patient's record?" "Can you integrate with an EHR?" "Are you ready for the US interoperability rules?" "How locked-in is your data?" "It's standard FHIR" answers all of them in one sentence. "We have a proprietary schema, but we could write an exporter" is a red flag that turns into a re-platforming line item. Building on FHIR isn't gold-plating — it's the table-stakes answer to questions you will be asked.
FHIR is the agent substrate, not the agent interface
This is the newest and most easily overclaimed reason. Agents — scribes, chart-summarizers, prior-auth bots — benefit when clinical facts land in typed, well-known resources instead of bespoke tables. FHIR resource types are in every model's training data; Observation, Condition, and MedicationRequest carry meaning a model already understands. A custom data_blob JSONB column carries none.
But our benchmark makes the boundary clear: raw FHIR dumps are a poor agent interface. Agent-readiness comes from a query-aware context layer over the FHIR substrate — bounded evidence packets, resolved references, temporal reducers, citations, and policy gates. The wedge we care about most is the AI medical scribe: a scribe can turn a conversation into structured clinical facts, but the next visit's agent context should be selected by the data layer, not improvised by dumping the whole chart into a prompt.
// The scribe pipeline, on FHIR primitives
const prep = await clinical.agent.sessionPrep({ patientId });
// → last visit, open problems, active meds, due assessments
await clinical.notes.create({
patientId,
text: draftFromTranscript, // SOAP/DAP, signed by the clinician
});
await clinical.observations.record({
patientId,
code: "8480-6", // Systolic BP (LOINC)
value: 128, unit: "mm[Hg]",
});
await clinical.assessments.record("PHQ-9", { patientId, score: 11 }); Under the hood each call writes a FHIR resource — a DocumentReference for the note, an Observation for the vital, a scored QuestionnaireResponse for the PHQ-9. Your agent starts from standard types; Bonfire's research track is the context compiler that selects the right bounded evidence before the model reasons. See the agent benchmark and the full clinical context report for the measured version of that claim.
What FHIR positions you for
Choosing FHIR isn't just defense. It gives you a real ecosystem to build toward, even though each integration still has implementation work:
- SMART-on-FHIR — the standard OAuth flow to launch your app from inside an EHR, with scoped access. FHIR gives you the substrate SMART expects; you still implement auth, scopes, registration, profiles, and conformance testing.
- Apple Health & device data — HealthKit can export clinical records as FHIR. Speak FHIR and patient-generated data has a standard target, though device feeds still need mapping and consent.
- HIEs and EHRs — health information exchanges and EHRs speak FHIR. Interop becomes a standards-aligned integration project, not a schema rewrite.
- Validators, terminology, and tooling — public FHIR validators, US Core profiles, and code systems like SNOMED CT (free for US use via the NLM UMLS license), LOINC, and RxNorm. You inherit shared meaning instead of inventing it.
- A clean export — when an enterprise asks for the patient's record,
clinical.fhir.export(patientId)returns a standard Bundle. No exporter project.
When you should NOT use FHIR
An honest guide has to draw the other line too. FHIR is overhead, and sometimes the overhead isn't worth it:
- Pure consumer wellness with no clinical interop. A meditation timer, a habit tracker, a fitness-streak app that never exchanges data with a provider — modeling a heart-rate log as a FHIR
Observationbuys you nothing. A plain column is correct. - Throwaway prototypes and internal tools. If you're validating a workflow this week and will rewrite the data layer anyway, don't model FHIR yet. Just don't let the prototype quietly become production.
- Data that genuinely isn't clinical. Billing line items, scheduling preferences, UI state, feature flags, app analytics — these aren't health resources and shouldn't be forced into FHIR. Keep them in normal app tables alongside your clinical layer.
- Operational scratch state. "Needs review" flags, draft autosave, assignment queues — FHIR has no good home for these, and that's fine. They belong in app state, not the clinical record.
Notice the pattern: skip FHIR for the parts of your app that are not the clinical record. The mistake is the inverse — building the clinical record on a custom schema to "move fast," then paying for it at the worst possible time.
"Build on FHIR" doesn't mean "build directly on a FHIR server"
Here's the nuance that trips teams up. FHIR is the right data model — but a raw FHIR server (HAPI, Aidbox, AWS HealthLake, and others) is an interoperability engine, not an application backend. Build your UI straight against one and you inherit no home for app state, no built-in access control, a terminology server to babysit — and, on managed cloud stores like HealthLake, search reads that lag your writes (OSS servers like HAPI or Medplum typically index in the write transaction). We cover that gap honestly in FHIR, explained for app developers, and we compare the FHIR servers head-to-head in our backend comparisons.
The pattern that works: FHIR underneath, app-native primitives on top. Your code says "create a note for this patient" and "give me this patient's timeline," with fresh-on-commit reads, real per-patient authorization, and a query you'd actually want — while FHIR R4 is the canonical record underneath so you can still export a Bundle and talk to an EHR. The data your app reads stays live and queryable:
// App-native read, FHIR underneath, fresh on commit
const { data: timeline } = useClinicalQuery(
clinical.notes.list({ patientId, status: "signed" })
); That's the whole idea behind a clinical backend like bonfireDB's FHIR layer (open-source core, with a managed BAA option, in early access): FHIR is the foundation and the export format, but you don't write FHIR JSON by hand to ship a list screen.
Keep reading
- FHIR, explained for app developers
- The backend an AI scribe actually needs
- The agent benchmark — why FHIR underneath is not the same thing as raw FHIR in the prompt.
- Agent context tools — the product layer above FHIR that returns bounded, cited evidence packets.
TL;DR
- Build the clinical record on FHIR. A custom schema doesn't dodge the complexity — it defers it into a painful migration.
- It's the Series-A expectation. "It's standard FHIR" answers interop, portability, and lock-in due-diligence in one sentence.
- It gives agents a real substrate. Agents start from standard resource types, but reliable agent reads still need query-aware clinical context above raw FHIR.
- It positions you for SMART, Apple Health, HIEs, validators, and SNOMED/LOINC. You still implement and test the integration surface.
- Skip FHIR for non-clinical parts — pure wellness, throwaway prototypes, billing, scheduling, UI/app state.
- "On FHIR" ≠ "directly on a FHIR server." Use FHIR as the model and export layer; build your app against app-native primitives on top.