One backend. Fresh reads, FHIR underneath.

bonfireDB puts Postgres at the center, keeps your common app reads fresh on commit, and generates FHIR R4 underneath for export and interop. One store, no Redis. You write clinical functions; the data layer behaves like a product backend, not a federation protocol.

The layered model

Four layers, one source of truth

FHIR was built to move records between distrustful organizations. When you build one product, that model leaks into everything. bonfire flips it: Postgres is the source of truth, FHIR is a generated projection on the way out.

1

Postgres — source of truth

Every clinical write lands in Postgres (with pgvector). Referential integrity, transactions, and write semantics work the way an app team expects — not the way a stranger-to-stranger exchange assumes.

2

Committed operational read models

Common app reads — notesByPatient, timeline, latestScores — are maintained on commit. No eventual-consistency lag, no rebuild-the-projection cron. The reads your list screens need are already fresh.

3

Reactive query cache

Clients subscribe with useClinicalQuery. When a write commits and a read model updates, the change is pushed to the client. The screen reflects the database — you don't wire up invalidation by hand.

4

FHIR R4 — generated underneath

FHIR R4 is produced from the same source of truth for export and interop, on demand. You get a clean Bundle when you need to leave or integrate — not a federation protocol you have to operate to ship a feature.

Sync vs async

Operational indexes are fresh. Intelligence is honest about timing.

Not every read can be free on commit. bonfire draws a hard line between the cheap reads your UI needs synchronously and the heavy work that can't be — and tells you exactly where each one stands.

Sync — fresh on commit

The operational read models that power list screens, timelines, and dashboards update inside the same commit as the write. By the time the write returns, these views are fresh.

  • notesByPatient
  • timeline
  • latestScores

Async — reports status

Embeddings, semantic search, agent-context assembly, and complex FHIR search run asynchronously. They don't block your write — but they don't silently rot either. Every write tells you they're pending and when they catch up.

  • semanticSearch
  • agentContext
  • complex FHIR search

The freshness lifecycle

Every write returns a freshness object. You never have to guess whether a read is current or whether a downstream index has caught up — the write tells you.

write-response.json
{
  status: "committed",
  views: {
    notesByPatient: "fresh",
    timeline: "fresh"
  },
  indexes: {
    semanticSearch: "pending",
    agentContext: "pending"
  }
}

What a write looks like

Call a typed function. Get a committed result, fresh operational views, and an honest status on the heavy indexes — in one response.

notes.ts
await clinical.notes.create({ patientId, encounterId, text })
// returns the freshness object above

const notes = useClinicalQuery(api.notes.listByPatient, { patientId })
// reactive, fresh on write
Why one store / no Redis

Postgres-first. The cache you didn't add can't drift.

The fastest way to get a stale read is to keep the same fact in two systems. Most stacks bolt a cache or a separate event bus next to the database, then spend the rest of their lives keeping the two honest. bonfire keeps one source of truth and gets freshness and reactivity from Postgres itself — no Redis in the default path.

1

Postgres is the source of truth

Clinical writes commit to Postgres (with pgvector). One transaction, one place a fact lives. There is no second copy to invalidate, so there is no window where the cache and the database disagree.

2

Committed operational read models — fresh on commit

The views your list screens read are maintained inside the same transaction as the write. When the commit returns, the read model is already current. That's the fast-read job a cache usually does — done with the database's own commit ordering, so it can't lag behind the write that produced it.

3

Reactive layer on LISTEN/NOTIFY

When a write commits, Postgres NOTIFY (and, for ordered streams, logical replication of the WAL) pushes the change outward in commit order. Subscribed clients update from the same timeline the database committed — no polling, no separate broker to keep in sync with the DB's state.

4

Async lane on SKIP-LOCKED — FHIR R4 underneath

Heavy work — embeddings, semantic indexing, agent-context assembly — drains from a Postgres queue with FOR UPDATE SKIP LOCKED. Workers pull jobs concurrently without a separate queue service, and FHIR R4 is generated from the same source of truth for export and interop. The whole pipeline is one database you already operate.

The two-database habit

  • Postgres for writes, Redis for fast reads → two copies of the same fact
  • A separate broker for events → its state can drift from the DB's
  • Cache invalidation wired by hand → the classic source of stale reads
  • More moving parts to run, secure, and put under a BAA

bonfireDB — Postgres-first

  • One source of truth; read models are committed, not cached
  • LISTEN/NOTIFY + WAL give commit-ordered freshness from the DB itself
  • SKIP-LOCKED runs the async lane without a queue service
  • Add a cache or bus only when a measured hot path forces it

Honest trade-off: Postgres-first isn't infinitely scalable on its own. The point isn't "never add a cache" — it's "don't start with one." When a measured hot path actually demands a cache or a dedicated bus, you add it deliberately, against real numbers, instead of inheriting a sync problem on day one.

Get running

Run it locally. Deploy to your AWS. Build.

Same code path the whole way. Start on your laptop, deploy into your own cloud, and write clinical functions in TypeScript.

1

Run it locally

Spin up the full stack — Postgres, pgvector, read models, reactive cache — in one command.

terminal
# local stack, one command — early access
2

Deploy to your AWS

Ship the same stack into your own AWS account. In the OSS tier it runs entirely in your infrastructure.

terminal
# deploy to your AWS — early access
3

Build

Import the typed client and call clinical functions. The freshness object comes back with every write.

app.ts
import { clinical } from "bonfire"
The two-databases problem, solved

App state and clinical data, in one place

FHIR has nowhere to put drafts, UI state, or workflow status — so teams stand up a second database and inherit a sync problem. And because you can't query FHIR by arbitrary fields, joins, or aggregates, even a list screen needs a denormalized projection. bonfire ends both.

FHIR-only, the usual way

  • No home for drafts, UI, or workflow state → a second DB
  • Two databases that must stay in sync → sync hell
  • No arbitrary field / join / aggregate queries
  • Every list screen needs a hand-built projection

bonfireDB

  • App state and clinical data live in one Postgres source of truth
  • No second database, no sync layer to operate
  • Query by any field, join, and aggregate — it's Postgres
  • List screens read committed operational views, fresh on commit
  • FHIR R4 still exports cleanly when you need it

Export is a function call, not a migration

Because FHIR is generated from the source of truth, leaving — or integrating — is one call away. You get a clean R4 Bundle on demand.

export.ts
await clinical.fhir.export(patientId)
// clean FHIR R4 Bundle on demand

Compliance & where it runs

OSS tier (Apache-2.0): bonfire runs entirely in your AWS account. The data never touches our infrastructure, so we are not a Business Associate — there's nothing for us to sign, because we never handle your PHI.

Managed tier: we host it for you and sign a BAA, backed by our own AWS BAA. Same code path, same freshness model, same FHIR-underneath — you just don't operate the infrastructure.

bonfireDB is early-stage; this page describes product design and positioning. "FHIR-native" / "FHIR-compatible" are used descriptively. Choose the tier that matches who you want holding the PHI.

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

Postgres source of truth, fresh-on-commit reads, FHIR generated underneath. Start in one command.

FAQ

Frequently asked questions

FHIR vs Postgres: which should I use for a clinical app backend?

Use Postgres as your source of truth and treat FHIR as an export format. FHIR was designed to move records between distrustful organizations, so it has no home for drafts, UI, or workflow state, and you can't query it by arbitrary fields, joins, or aggregates. bonfireDB writes to Postgres and generates FHIR R4 underneath on demand, so you get app-style querying plus clean interop.

Does bonfireDB need Redis or a separate cache?

No. There's no Redis in the default path. bonfireDB maintains committed operational read models inside the same transaction as the write and uses Postgres LISTEN/NOTIFY (plus WAL for ordered streams) for reactivity, so freshness comes from the database itself. You add a cache later only if a measured hot path forces it.

How does bonfireDB keep reads fresh on commit?

Common app reads like notesByPatient, timeline, and latestScores are maintained inside the same commit as the write, so by the time the write returns those views are already current. bonfireDB is designed so there's no eventual-consistency lag and no projection-rebuild cron for the operational reads your list screens need.

What does a write return in bonfireDB?

Every write is designed to return a freshness object: a committed status, which operational views are fresh, and the status of heavy indexes like semanticSearch and agentContext. Sync read models come back fresh; async work (embeddings, semantic search, agent context) reports pending and tells you when it catches up, so nothing silently rots.

Is bonfireDB a HealthLake or FHIR-server alternative?

bonfireDB is the app backend that generates FHIR underneath (not a FHIR server), not another FHIR server. Instead of operating a federation protocol to ship a feature, you write typed clinical functions over Postgres and get FHIR R4 Bundles generated on demand for export and interop. It's early access (Apache-2.0 OSS tier runs entirely in your own AWS account).