An AI medical scribe listens to a visit and writes a SOAP note. The demo takes a weekend. The real work is everything underneath: storing audio and transcripts as PHI, turning an AI draft into a clinician-signed, immutable note, recording who actually authored each version, and proving all of it to an auditor. This guide covers the data model an AI scribe actually needs — and the parts you can't fake.
What backend should an AI medical scribe use?
An AI medical scribe should use a clinical backend that stores data as FHIR R4 — transcripts and notes as DocumentReference, audio as Media/Binary, structured findings as Observation — behind a signed BAA, with a tamper-evident audit trail and provenance that distinguishes AI-generated drafts from clinician-signed notes. You vibe-code the scribe UI; you do not vibe-code that data layer.
Where transcripts and SOAP notes actually go
A scribe produces three kinds of artifact, and each has a correct FHIR home. Putting them in the right resource is what makes the data portable, queryable, and defensible later.
- The audio recording → a
Mediaresource (with the bytes held as aBinary), linked to the encounter. It is PHI the moment it is captured, so it lives encrypted in your cloud — never in a non-BAA bucket or a third-party transcription service that hasn't signed one. - The raw transcript → a
DocumentReferencewith a transcripttypeandstatus: current, pointing at the encounter. Keep it; it is your evidence for how the note was generated. - The SOAP/DAP note → a
DocumentReferencefor the document itself, or aCompositionwhen you want the note structured into sections (Subjective, Objective, Assessment, Plan) the way a clinician thinks. ACompositionis the FHIR way to say "this is an authored, sectioned clinical document with an author and a date."
With bonfireDB the scribe writes a draft note in one call, and the FHIR mapping happens underneath:
// AI scribe finishes a visit: transcript in, draft note out.
const draft = await clinical.notes.create({
patientId,
encounterId,
format: "SOAP",
text: aiGeneratedNote, // becomes a DocumentReference / Composition
source: {
transcriptId, // links back to the stored transcript
model: "scribe-v2", // recorded as provenance, NOT as the author
status: "draft", // not a signed clinical note yet
},
}); The data model a scribe actually needs
A working scribe is not a notes table. It is a small graph of clinical resources, and the relationships are what give the note meaning. The minimum:
Patient— the subject of the visit, with a stable identifier the rest of the graph references.Encounter— the visit itself (date, type, location, clinician). Every artifact the scribe produces hangs off the encounter, not loosely off the patient.- Transcript (and audio) — the source material, stored as above and linked to the encounter.
- DraftNote → SignedNote — two states of the same document. The draft is editable and AI-authored; the signed note is immutable and clinician-authored. The transition is the whole game (next section).
- Structured findings — vitals or scores the scribe extracts (e.g. a blood pressure, a PHQ-9 result) land as
Observations, not buried in note text, so they're queryable and trendable. Provenance+AuditEvent— the record of who and what: which model drafted, which human edited, who signed, who later read it. This is not optional metadata; it is the audit trail.
Recording a finding the scribe pulled out of the conversation is one call, and it becomes a real, searchable Observation:
await clinical.observations.record({
patientId, encounterId,
code: "blood-pressure",
value: { systolic: 128, diastolic: 82, unit: "mmHg" },
});
// A screener the scribe administered, scored as a FHIR Observation:
await clinical.assessments.record("PHQ-9", {
patientId, encounterId, score: 11,
}); The part you can't fake: provenance, signing, and audit
This is where most scribe MVPs are quietly non-compliant. An AI draft is not a medical record. It becomes one only when a licensed clinician reviews and signs it — and the system has to record that truthfully.
- AI-vs-clinician provenance. The model is the source of a draft; it is never the author of a clinical note. The clinician who signs is the author of record. Store both: a
Provenanceresource that says "drafted by scribe-v2 from transcript X, edited by Dr. Lee, signed by Dr. Lee." Conflating the two — listing the AI as author, or hiding that AI was involved — is exactly the kind of thing an audit will surface. - Signed-note immutability. Once signed, a note cannot be silently edited. Corrections happen as a FHIR amendment (a new versioned entry that links to the original), not an in-place overwrite. Your backend has to enforce this, because "let the user edit the note" is the default an AI tool will generate.
- Audit trail. Every read, write, sign, and amend produces an
AuditEvent— automatically, not because someone remembered to log it. When a patient or regulator asks "who saw this and when," the answer has to already exist. - A BAA on every PHI vendor. The transcription API, the LLM, the database, the storage bucket, the logging pipe — each one that touches the audio, transcript, or note needs its own signed BAA. A BAA on your coding tool covers the tool handling your code, not your running app's data. We go deeper on this in can you vibe-code a HIPAA-compliant app.
The signing transition, done right, is one explicit call that flips the document to immutable and writes the provenance and audit entries for you:
// Clinician reviews the AI draft, edits, and signs.
const signed = await clinical.notes.sign(draft.id, {
signedBy: clinicianId, // author of record
// -> note becomes immutable
// -> Provenance: drafted-by(model) + signed-by(clinician)
// -> AuditEvent emitted automatically
}); Reading it back, prepping the next visit, and getting out
A scribe isn't write-only. Clinicians read prior notes, the AI needs context for the next encounter, and you eventually need your data in a portable format. Three calls cover the common cases:
// Live-updating note view in the UI:
const { data: notes } = useClinicalQuery(
clinical.notes.list, { patientId }
);
// Give the model grounded context before the next visit:
const context = await clinical.agent.sessionPrep({ patientId });
// Portability is not optional — export real FHIR R4:
const bundle = await clinical.fhir.export(patientId); sessionPrep matters specifically for scribes: a good draft depends on the model knowing the patient's history, current problems, and prior plan — and pulling that context through an authorization-aware, BAA-covered path instead of dumping the whole chart into a prompt. That path is exactly what bonfire's agent layer (MCP + clean projections) is designed to provide, and why it's the part worth measuring: agents working over raw FHIR cap out around 50% on real tasks (FHIR-AgentBench), so the context call has to do better than the raw resource graph. FHIR export matters because a clinical record you can't take with you is a liability, not an asset.
Build vs. buy the data layer
You will build the scribe — the capture, the prompt, the review UI. That's your product. The question is whether you build the clinical data layer beneath it, and at pre-seed the honest answer is usually no.
- Building it yourself means hand-modeling FHIR resources, implementing signed-note immutability and amendments, wiring provenance and an audit trail, getting per-patient authorization right, and signing a BAA with every PHI vendor — before you've validated the scribe. Months of regulated infrastructure, none of it your differentiator.
- AWS HealthLake is a real, mature FHIR datastore and the sensible enterprise answer. It is also priced and shaped for enterprise: you still build the scribe-specific model (drafts, signing, provenance) on top, and the integration weight is real for a two-person team. See the honest bonfireDB vs. AWS HealthLake comparison.
- bonfireDB is the pre-seed/indie answer: an open-source clinical backend (TypeScript + Postgres + pgvector, FHIR R4 underneath, no Redis), BAA-from-day-one via the managed option, and agent-native SDK calls like the ones above. The data model a scribe needs — Encounter, transcript, draft-to-signed note, provenance, audit, FHIR export — is the product, not a project.
The framing is deliberate: build the workflow, buy the regulated data layer. Other reframes of code-first FHIR exist (Medblocks has done strong work here) — pick whoever fits, but don't build the layer from scratch to learn what FHIR signing and audit cost.
Keep reading
- Build an AI scribe in a weekend — the step-by-step build that puts this data model to work.
- The agent benchmark — why agents over raw FHIR cap out around 50%, and what the projection layer is meant to fix.
TL;DR
- An AI scribe's backend is FHIR underneath: transcript + note as
DocumentReference/Composition, audio asMedia/Binary, findings asObservation. - The data model is a graph — Patient → Encounter → transcript → DraftNote → SignedNote — not a notes table.
- The parts you can't fake: AI-vs-clinician provenance, signed-note immutability (corrections as amendments), an automatic audit trail, a BAA on every PHI vendor, and FHIR export.
- HealthLake is the real enterprise option; bonfireDB is the pre-seed/indie one — BAA-from-day-one and agent-native. Build the scribe, buy the clinical data layer.
- Start with the scribe backend overview and the security & HIPAA model; compare options in comparisons.