First-party signed kind:store plugin cdylib: the Postgres backend for busbar's durable governance store, exported over the store C ABI. Drop the built library into the plugins folder and set store.module: postgres to share virtual keys, budgets, and usage across a fleet of busbar nodes.
| kind | alias | crate | busbar | license |
|---|---|---|---|---|
store |
postgres |
busbar-store-postgres-plugin |
1.6.0 (pinned in .busbar-ref) |
Apache-2.0 |
This plugin's version: v1.0.0. (Independently versioned from busbar itself — see Versioning below.)
The first-party, signed kind: store plugin for
busbar: the Postgres backend for busbar's
durable governance store, exported over the store C ABI. Drop the built
.so/.dylib/.dll into the engine's plugins folder and set
store: { module: postgres, settings: { url: "postgres://..." } }; the
engine loads it in-process at boot.
One Postgres behind a fleet of busbar nodes means virtual keys,
budgets, and usage are shared across the cluster instead of siloed per
node.
This plugin is versioned independently of busbar — v1.0.0 here says
nothing about which busbar release it is. Compatibility with busbar is
stated separately: requires busbar 1.6.0+ (the release whose store
interface this crate implements: the kind-tagged plane-record verbs, the
name-keyed usage ledger and the dated metering rows). Pin both versions
explicitly in production; do not assume they move together.
The first connect of this build upgrades an existing database in place (schema v6 → v10, inside one transaction, under the same advisory lock every node's migration takes). Nothing is dropped and no existing row changes meaning:
keysgainsidp_subject,binding_mode,minted_by(NULL for existing keys) andallowed_scopes_by_kind, which holds non-pool scope grants (mcp_server,agent, …). Pool grants stay inallowed_poolsexactly as 1.5.x wrote them.usage_meteringgainspriced_from_ms(existing rows read0, the opening rate card) and it joins the primary key, so a rate-card change mid-day opens a second row for that day.- New tables:
usage_ledger_unitsandusage_metering_units(unit classes other than the four reserved token classes), andplane_records,plane_chain,plane_tokens(busbar's durable A2A tasks, MCP call log, upstream demotions, push-callback capabilities and single-use approvals).
A database a pre-release dev build took to schema v7–v9 keeps its
mcp_calls/tasks/task_events/mcp_demotions/spent_ask_states tables
untouched; busbar 1.6.0 no longer reads them.
It is a cdylib that implements busbar's RecordStore trait (via the plugin SDK in
busbar-contract)
and is loaded in-process by busbar over the signed hybrid plugin ABI —
dlopen'd, not spawned as a separate process.
This repo is a same-repo, 2-crate Cargo workspace: it brings 100% of
what it needs. All the SQL and schema logic lives in the
busbar-store-postgres crate (store-postgres/, a same-repo sibling
this plugin wraps — a custom build can also link it statically instead
of loading this cdylib); store-postgres-plugin/src/lib.rs only adapts
the engine's JSON config ({"url": "postgres://..."}) into a
PostgresStore.
- A shared, multi-node governance store.
store: sqliteis per-node;store: postgresputs virtual keys, budgets, and usage behind one database so a fleet of busbar nodes agrees on state. - Interchangeable with the SQLite backend for everything busbar
itself does: the same keys, credentials, tombstone and revision
semantics, and the same JSON encoding of
allowed_pools. Two caveats worth knowing before you write anything against it directly. - The physical tables differ. Postgres keeps per-model token
counters in their own
usage_ledgertable, where SQLite folds them intousage_windowswithmodelin the primary key. Write reporting queries and ETL against the backend you are actually running, never against the assumption that the two are byte-identical. - A few error cases differ across backends. Revoking a credential
id that names no row is an error here and on MySQL, and a silent
success on SQLite and Valkey. Appending an audit entry whose
seqalready exists is an error here, where the other backends overwrite or ignore. Tooling that treats a store error as fatal should not assume the same input produces the same outcome on every backend.
- No TLS in this build (
NoTls). Run the connection over a trusted network segment, a local socket, or a TLS-terminating proxy (pgbouncer/stunnel). - No automatic reconnect. A persistently dropped connection surfaces as store errors on the write-behind flush path and on admin operations; a permanently broken connection requires a process restart (let your supervisor handle it).
See the doc comments at the top of
store-postgres/src/lib.rs
for the full design rationale — that is where the actual store logic
lives (in this repo now, not busbar); store-postgres-plugin/ is the
thin cdylib adapter around it.
| Setting | Required | Default | Notes |
|---|---|---|---|
url |
yes | — | A libpq connection string, e.g. postgres://user:pass@host:5432/busbar. Connects NoTls; run it over a trusted network segment or a TLS-terminating proxy. No connect timeout is set by default — a blackholed host wedges engine boot indefinitely. libpq honors a connect_timeout query param in the DSN, e.g. postgres://user:pass@host:5432/busbar?connect_timeout=10; set one if boot hanging on a dead host is a concern. |
Needs a Rust toolchain (rustup), and — interim,
until busbar ships publicly
— a sibling checkout of busbar at ../busbar (see
Dependencies below).
cargo build --workspace --release # cdylib: target/release/libbusbar_store_postgres_plugin.{so,dylib}
cargo test --workspace # the end-to-end loader tests (see store-postgres-plugin/tests/e2e.rs) — need a real Postgres, see below
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --checkThis is a same-repo, 2-crate Cargo workspace (store-postgres/, the
real logic crate — which also carries the plugin's one door
registration, export_store_plugin!(open), and its linked row — and
store-postgres-plugin/, the thin cdylib adapter that re-exports it;
see members).
Its one busbar dependency is busbar-contract (plus
busbar-plugin-loader, dev-only, for the conformance and end-to-end
tests): a git dependency on
GetBusbar/busbar pinned to the
rev in field 1 of .busbar-ref. No sibling-checkout
path dependency ships in any manifest; CI's pin job refuses one, and
refuses a manifest rev that disagrees with .busbar-ref.
The end-to-end tests drive the REAL busbar binary, built from a
busbar checkout at that same rev: BUSBAR_CHECKOUT=<path>, or a
busbar/ checkout beside this repo (CI checks one out there).
store-postgres-plugin/tests/conformance.rs holds the store to ONE
row and ONE behaviour through both doors — linked (the linked::STORE
boundary) and dropped in (this repo's cdylib, signed, packed, scanned
and opened by the real loader) — with RED arms that prove the
comparison is not vacuous.
Unlike a kind: hook plugin, this store's only meaningful coverage is
against a live Postgres — there is no useful mock for "did the SQL
actually persist."
store-postgres-plugin/tests/e2e.rs installs the plugin the way an
operator does: it packs the built cdylib into a real tarball with the
same tool the release signs, drops it into a real plugins.dir, and
boots a real busbar process against store: { module: postgres }.
That boot runs against a disposable, freshly created database, so the
schema it finds afterwards can only have come from the boot under test.
Against a shared database the same check would pass whether or not the
plugin ever loaded.
store-postgres-plugin/tests/admin_api_e2e.rs goes one step further:
it installs the plugin over the real admin API, restarts onto it, mints
a key with an AWS-shaped credential over that API, and reads both rows
back with a raw client that never touches the plugin, the C ABI or the
loader.
store-postgres/src/tests.rs holds the store's own coverage against a
live database: tombstone semantics, slot-safe credential minting,
revocation, snapshot isolation, and each migration boundary on its own
throwaway database (including a released 1.5.x database upgraded in place).
src/tests/plane_records.rs covers the plane-record verbs, and
src/tests/store_conformance.rs is this repo's own copy of busbar's Store
conformance suite, taken verbatim from busbar's in-tree copy and wired in
full.
All of them are gated on the BUSBAR_TEST_POSTGRES_URL env var, and
they refuse to skip silently under CI. An unset variable there is a
hard failure, not a quiet pass:
# Point at any reachable Postgres 16+ database:
export BUSBAR_TEST_POSTGRES_URL=postgres://busbar:busbar@localhost:5432/busbar_test
cargo test --workspaceLocally, with the env var unset, most live cases print a skip:
message and pass. The trust-state cases (demotions, the single-use token
ledger, push-callback liveness — in plane_records.rs and the
trust_state_… e2e case) deliberately FAIL instead of skipping, because
their unimplemented form is silently green; run them against a database. Under
CI (CI env var set — see .github/workflows/ci.yml), a missing
BUSBAR_TEST_POSTGRES_URL is a hard failure, not a silent skip:
CI provisions a real postgres:16 GitHub Actions service container on
every push, specifically so this coverage can never quietly vanish.
Once built, the cdylib is packed and signed like any other busbar
plugin — see
docs/plugins.md
in busbar for the full reference. In short:
BUSBAR_SIGN_KEY=<signing key> busbar-plugin-pack pack \
--lib target/release/libbusbar_store_postgres_plugin.so \
--name busbar-store-postgres-plugin --alias postgres --kind store \
--version 1.0.0 --publisher busbar \
--license Apache-2.0 \
--out busbar-store-postgres-plugin-1.0.0-x86_64-linux.tar.gzFor local development without a signing key, busbar-plugin-pack pack --allow-unsigned produces a tarball busbar loads only under
plugins.trust.allow_unsigned: true.
Drop the resulting tarball into busbar's configured plugins.dir and
set:
store:
module: postgres
settings: { url: "postgres://user:pass@host/busbar" }— see docs/configuration.md
for the full store config reference.
cargo test --workspace --lockedLicensed Apache-2.0 (LICENSE). Contributions welcome — see CONTRIBUTING.md. Governed by our Code of Conduct; security issues go through SECURITY.md, not public issues.