Always fresh

Common app reads are fresh on commit — the moment a write lands, not 15–30s later. No polling, no manual cache invalidation. Heavy work runs async and tells you exactly where it stands.

The sync / async split

Some reads can't wait. Some reads shouldn't block.

FHIR servers treat every change the same way: write it, then wait for the index to catch up. That works when records crawl between distrustful hospitals. It falls apart when one team is building one product and a clinician just signed a note. bonfireDB splits the difference — the reads your UI needs every second are fresh on commit, and the expensive work happens off the commit path with a status you can watch.

A FHIR server

Write the resource, then wait for eventual consistency before search reflects it. AWS HealthLake, for one, documents a default eventual-consistency window of ~15–30s between a write and when it's searchable. Realtime is best-effort: rest-hook delivery can silently drop, events arrive out of commit order, bulk $import emits zero events, and the channel can fall days behind.

  • List screen shows stale data right after a write
  • You poll, refetch, or hand-invalidate caches yourself
  • Your search index and analytics silently rot

bonfireDB

Postgres is the source of truth. Operational read modelsnotesByPatient, timeline, latestScores — are kept fresh on commit, inside the same transaction, so common reads reflect the write the instant it lands. A reactive query cache pushes the new value to the client. Heavy work runs async with reported status.

  • List screens reflect the write immediately
  • No polling, no refetch, no manual invalidation
  • Indexes report pending → fresh, never silently rot

The freshness lifecycle

Every write tells you what's fresh and what's still cooking

A write doesn't return a bare 200. It returns a freshness object: the commit status, which operational views are already fresh, and which heavy indexes are still catching up. You decide what to render now and what to wait on — without guessing.

notes.ts
const result = await clinical.notes.create({ patientId, encounterId, text })

// result is the freshness lifecycle object
{
  status: "committed",
  views: {
    notesByPatient: "fresh",
    timeline: "fresh"
  },
  indexes: {
    semanticSearch: "pending",
    agentContext: "pending"
  }
}

Operational views are fresh on commit. Heavy indexes (semantic search, agent context, complex FHIR search) run async and report pending until they catch up — so you always know the truth instead of assuming it.

How it works — Postgres-first

Postgres does the freshness. There's no Redis to run.

Freshness isn't a separate cache or message bus you stand up alongside the database. It's the database. bonfireDB is TypeScript + Postgres + pgvector, with FHIR R4 generated underneath — and Postgres alone carries commit-ordered freshness. No Redis. No second system to deploy, secure under a BAA, keep consistent, and page someone about at 2am.

Fresh on commit

Operational read models are updated in the same transaction as the write. When the commit lands, the view is already fresh — there's no window where the write succeeded but the read is stale.

Commit-ordered push

Postgres LISTEN/NOTIFY plus the WAL via logical replication drive the reactive query cache — so clients are notified in commit order, not best-effort and out of sequence like rest-hooks.

Async lane, same DB

Heavy work (embeddings, semantic index, agent context) drains from a Postgres work queue with FOR UPDATE SKIP LOCKED — durable, concurrent, and idempotent, with no extra broker to operate.

Postgres-first by default: LISTEN/NOTIFY + WAL/logical-replication for commit-ordered freshness, a SKIP LOCKED queue for the async lane. We add a separate cache or bus only when a measured hot path forces it — not on day one, not by reflex.

Reactive reads with useClinicalQuery

Subscribe once. When a write updates an operational view, Postgres LISTEN/NOTIFY drives the reactive query cache and the new value is pushed to the client. No refetch, no polling, no invalidation logic to maintain.

  • Reads stay live — the UI re-renders when the underlying view changes
  • Fresh on commit, because the view is updated in the write transaction
  • You write a query, not a cache-management subsystem
PatientNotes.tsx
// reactive, fresh on commit — no refetch / poll / invalidation
const notes = useClinicalQuery(
  api.notes.listByPatient,
  { patientId }
)

// when clinical.notes.create commits,
// this view is fresh and re-renders automatically

Heavy work, off the commit path

Expensive reads run async — and report status

Semantic search, embeddings, agent context assembly, and complex FHIR search are too heavy to block a write. They run asynchronously off the commit path and report pending → fresh through the same freshness lifecycle. Your write stays fast; your indexes never silently fall behind.

On commit

Operational read models like notesByPatient, timeline, and latestScores are kept fresh as part of the write.

Async, reported

Semantic search and embeddings rebuild off the commit path, surfacing pending until they're caught up.

Semantic search →

Agent context

Cited, permission-aware context for agents is assembled async — never blocking the clinician's write.

Custom MCP builder →

FHIR underneath

FHIR R4 is generated underneath for export and interop, on demand — not on the hot path of every read.

FHIR underneath →

Assemble agent context without blocking the clinician

The same freshness model powers the agent layer. Session prep pulls a cited, permission-aware window of the record — assembled async, off the write path, so the moment a note commits the clinician keeps working while heavier context catches up.

  • Cited to source records, scoped by permission
  • Runs async and reports status — never stalls a write
  • Agents read clean projections, never raw FHIR by default
See the custom MCP builder →
sessionPrep.ts
// cited, permission-aware — assembled async
const ctx = await clinical.agent.sessionPrep({
  patientId,
  windowDays: 90,
  include: ["recentNotes", "assessments", "tasks"]
})

// clean FHIR R4 Bundle on demand, not on the hot path
await clinical.fhir.export(patientId)

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

Common reads fresh on commit, heavy work async with honest status, FHIR generated underneath. Stop operating a federation protocol — start shipping your app.

FAQ

Frequently asked questions

How do I get fresh reads on commit without eventual-consistency lag?

bonfireDB keeps operational read models like notesByPatient, timeline, and latestScores fresh inside the same transaction as the write, so common reads reflect the change the instant the commit lands — no 15–30s eventual-consistency window. This is a design goal of the early-access product, not a benchmarked claim.

Why does AWS HealthLake have a delay before a write is searchable?

As of 2026 (verify current), AWS HealthLake documents a default eventual-consistency window of roughly 15–30 seconds between a write and when it becomes searchable, because FHIR servers rebuild the search index after the write. bonfireDB instead keeps common app reads fresh on commit and runs only heavy indexes async. It's an app backend that generates FHIR underneath (not a FHIR server), not another FHIR server.

How does bonfireDB do reactive reads without Redis?

It uses Postgres LISTEN/NOTIFY plus the WAL via logical replication to push new values to a reactive query cache in commit order. There's no Redis or message bus to deploy — bonfireDB is designed as TypeScript + Postgres + pgvector, so Postgres alone carries commit-ordered freshness.

What happens to semantic search and embeddings if they aren't fresh on commit?

Heavy work like semantic search, embeddings, and agent context runs async off the commit path and reports pending → fresh through a freshness lifecycle object returned by each write. Indexes report their status honestly instead of silently falling behind, so you always know what's caught up.

Can I subscribe to live clinical query results in the UI?

Yes — useClinicalQuery subscribes once and re-renders when the underlying operational view changes. When a write commits, Postgres LISTEN/NOTIFY drives the reactive cache and the new value is pushed to the client, with no refetch, polling, or manual cache invalidation to maintain.