🛡️ Clinical authorization & audit

Bring your own auth. We own the part FHIR never solved: who can see which patient, under what consent, for how long — designed to be enforced on every read and write, with an AuditEvent for each.

The distinction that matters

Authentication isn't the hard part. Clinical authorization is.

You already have an identity provider you trust. What you don't have is a layer that knows a clinician is assigned to this patient, in this tenant, under a consent that's still live — and refuses the query otherwise.

Bring your own auth

Clerk, Auth0, Cognito, WorkOS — keep whatever issues your tokens and manages users. bonfire reads the verified identity; it never tries to replace your IdP.

We own clinical authorization

Patient–clinician assignment, tenancy isolation, minimum-necessary scoping, and agent permissions. The decisions FHIR's data model leaves entirely up to you.

Read and write

Every clinical primitive runs through the same authorization gate going in and coming out. There's no GraphQL door, no $export door, no history door that skips it.

FHIR is a data model, not an access-control system

FHIR gives you security-labels and a Consent resource — but no engine that enforces them. The model describes intent; nothing acts on it.

  • Security-label masking leaks through the side doors — GraphQL, $export, and history can return what the read API hid.
  • Masking is usually a read concern; the write path is often left ungated — so a blind PUT can clobber a field a concurrent writer just changed (a lost update), and nothing checked whether the caller was allowed to write that patient at all.
  • Consent is a schema with no enforcement engine — storing a Consent resource doesn't stop a single query.
  • HealthLake has no per-resource ABAC: IAM gates the whole datastore, not a patient or a field.

So you end up writing the authorization layer by hand — and any path you forget becomes a leak.

without an engine
// the resource has the right label…
{ "resourceType": "Observation",
  "meta": { "security": [
    { "code": "R" } // restricted
  ] } }

// …but who actually enforces it?
GET /Observation?patient=123      // masked ✓
GET /Observation/$everything      // ?
POST /graphql { observations {…} } // ?
GET /Observation/o-9/_history     // ?
One gate, every path

Authorization is a function of the request, not a property of the response

Define clinical access once. bonfire enforces it on reads, writes, exports, history, and agent calls — there is no path that bypasses the policy.

policy.ts
clinical.access.policy({
  // tenancy: a request can only ever touch one tenant's data
  tenant: (ctx) => ctx.orgId,

  // patient scope: clinician must be assigned to the patient
  canReadPatient: (ctx, patientId) =>
    clinical.assignments.exists({ clinician: ctx.userId, patientId }),

  // minimum necessary: scope which clinical kinds a role may read
  minimumNecessary: {
    front_desk: ["appointments", "demographics"],
    clinician:   ["notes", "assessments", "observations"],
  },

  // consent as a live engine — not just a stored resource
  consent: (ctx, patientId) =>
    clinical.consent.active({ patientId, purpose: "treatment" }),

  // time-bound break-glass: emergency access that expires + is flagged
  breakGlass: { ttlMinutes: 60, requiresReason: true },
});

Every clause runs on both directions. A write that violates tenancy or minimum-necessary is refused the same way a read is — the policy doesn't care which verb you used.

Consent that does something

In FHIR, a Consent resource is a document you store. In bonfire it's a live engine: the policy evaluates active consent at query time, and a withdrawn or expired consent closes the door immediately — across every path.

  • Consent is evaluated per request, not assumed because a resource exists.
  • Withdraw consent and the next read fails closed — including $export and history.
  • Purpose-of-use is part of the decision (treatment vs. research vs. billing).

The honest trade-off: FHIR's Consent resource is genuinely expressive — bonfire reads that model rather than inventing a parallel one. What we add is the engine that evaluates it on every request.

Time-bound break-glass

Emergencies need access that normal scoping would deny. Break-glass grants it — but it's time-bound, reason-required, and loud: every break-glass access writes an AuditEvent flagged as such, and the grant expires on its own.

consent & break-glass
// withdraw — closes every path on the next request
await clinical.consent.withdraw({
  patientId, purpose: "research",
});

// emergency access, scoped + expiring
await clinical.access.breakGlass({
  patientId,
  reason: "ED admission, charts needed",
}); // → audited, flagged, expires in 60m
Audit you don't have to remember to write

Every operation is designed to produce an AuditEvent — automatically

Azure's FHIR service doesn't auto-write AuditEvents; you have to build that yourself. In bonfire, the audit record is part of the operation. There's no code path that touches a patient and forgets to log it.

Auto AuditEvent

Every read, write, export, and agent call emits an AuditEvent with actor, patient, purpose, and outcome — without you wiring it.

Provenance

Writes carry provenance back to their source. You can answer "where did this value come from" without reverse-engineering it.

What the agent read

Agent reads are audited too — including which records an agent pulled to build its context. Not just "an agent ran."

No bypass

Because audit is part of the gate, the side doors that leak elsewhere (GraphQL, export, history) are logged here like any other read.

Agents are first-class subjects of the policy

An agent calling a clinical tool is just another caller through the same gate — patient- and tenant-scoped automatically, reads audited (including what it read), and writes are propose-only by default. A human approves before anything touches the record.

  • Every agent tool is patient/tenant-scoped without per-tool wiring.
  • canWrite: "propose-only" — the agent drafts, a clinician signs.
  • Every agent read is audited and every result is cited to its source record.

Pairs with the custom MCP builder — the same policy that gates your SDK and HTTP endpoints gates your agent tools.

agent.policy.ts
clinical.agent.policy({
  // inherits tenant + patient scope from access.policy
  scope: "per-patient",

  // agents never write directly — they propose
  canWrite: "propose-only",

  // every read is logged with the records touched
  auditReads: true,

  // every answer must cite its source record
  requireCitations: true,
});

// agent context is permission-aware + cited
const ctx = await clinical.agent.sessionPrep({
  patientId,
  windowDays: 90,
  include: ["recentNotes", "assessments", "tasks"],
});
Side by side

What "enforced" actually means

CapabilityFHIR data model aloneHealthLakebonfireDB
Patient–clinician scopingYour job to buildIAM gates whole datastoreBuilt-in, on read + write
Minimum-necessarySchema onlyNoneRole-scoped policy
Consent enforcementResource, no engineResource, no engineLive engine, per request
Masking via export/history/GraphQLCan leakCoarse / N/ASame gate, no bypass
AuditEvent on every opManualPartialAutomatic
Agent writesUnconstrainedUnconstrainedPropose-only by default

bonfireDB is early-stage; this page describes product design and positioning. Comparisons reflect each system's stated authorization model, not a benchmark.

Where this fits

Authorization is the spine the rest of the backend runs through

App-native primitives

Typed clinical functions that all run through the access policy.

Explore →

Custom MCP builder

Agent tools that inherit ABAC scoping, audit, and citations.

Explore →

Semantic search

Per-hit authorization so results respect minimum-necessary.

Explore →

FHIR underneath

Exports flow through the same gate as every other read.

Explore →

Wiring this into a vibe-coded app? How to vibe-code a HIPAA-sensitive app covers where the access gate, consent engine, and audit trail have to live so the parts you generate don't leak PHI.

You build the app. Bonfire is the clinical data layer underneath.

Bring your auth. Let bonfire own the patient scoping, consent engine, and audit trail — enforced on every path, by default.

FAQ

Frequently asked questions

What is clinical authorization and why doesn't FHIR handle it?

Clinical authorization decides who can see which patient, under what consent, and for how long. FHIR® gives you security-labels and a Consent resource but no engine that enforces them, so bonfireDB is designed to own that layer above the FHIR data model, enforced on every read and write.

How does bonfireDB do per-patient access control (ABAC)?

You define a policy once with patient-clinician assignment, tenancy isolation, and minimum-necessary role scoping. bonfireDB is designed to evaluate it on reads, writes, exports, history, and agent calls so there's no side door (GraphQL, $export, or history) that bypasses the gate.

Does bonfireDB log an audit trail automatically?

Yes, by design. Every read, write, export, and agent call is intended to emit a FHIR AuditEvent with actor, patient, purpose, and outcome, because the audit record is part of the operation rather than something you wire up. Unlike services where you build auditing yourself, there's no path that touches a patient and forgets to log it.

How is bonfireDB different from HealthLake for access control?

As of 2026 (verify current), HealthLake's IAM gates the whole datastore, not an individual patient or field, and consent is a stored resource with no enforcement engine. bonfireDB is designed for built-in patient-clinician scoping, role-based minimum-necessary, and a live consent engine evaluated per request.

How does bonfireDB constrain what AI agents can read and write?

Agents are first-class subjects of the same policy: patient- and tenant-scoped automatically, every read audited with the records touched, answers cited to their source, and writes propose-only by default so a clinician signs before anything touches the record.