Governance
Governance gives you a control plane over who can use which pools and how much. Enforcement happens before the request is forwarded: the limit check and the charge are a single atomic operation, and a request that would exceed a cap or a per-minute rate limit is rejected at the gate, not reconciled after the bill. It’s off by default; turn it on when you need budgets, rate limits, spend visibility, or access control. Each capability below has two views; read What it does for the why, flip to Configure it for the exact YAML and commands.
The 1.5.0 model rests on three ideas: a virtual key is a signed, expiring bearer token that
carries pure identity; the groups: tree is the one place every limit lives (budget, requests,
tokens, concurrency), hierarchical so a user’s personal budget is always sub-capped by their team’s
ceiling; and the top-level rate_card is the one cost source, so spend is derived from the
token ledger at read time, never stored as a ledger charge.
Virtual keys, budgets & spend
Section titled “Virtual keys, budgets & spend”Busbar issues virtual keys: signed, expiring bearer tokens your applications present instead of a raw client token. A key is pure identity — it carries no limits of its own. Every limit lives on the group the key is bound to, and the key charges through that group’s chain.
A key binds to at most one group at mint. On each request Busbar walks the group chain up
through parent, enforcing every limit of every group in the chain (an AND, charged
all-or-nothing in a single atomic operation), so a user’s personal budget can never sum past
their team’s ceiling. Spend is derived, never stored: the group ledger accumulates the raw
token split (input / output / cache-read / cache-write) and Busbar re-prices it at read time from
the current top-level rate_card plus the flat per_request_fee. A rate correction re-prices
history on the next read; you bill from the raw token counts.
The check and the charge are a single atomic operation, so concurrent requests cannot race past a budget; only a single admitted request’s post-response token cost can marginally overshoot. If the store errors on admission, Busbar fails open by default (traffic keeps flowing) and can be configured to fail closed.
Why it matters: you get per-key and per-team cost attribution and caps without trusting every developer and deployment to self-police. Keys are signed and expire (1.x keys never did); revoking or rotating a key takes effect immediately. The signed token is shown in plaintext exactly once.
Scope: the unit of control is the group. A user is modeled as a leaf group parented to their team — the personal budget is the leaf’s own limits, always sub-capped by the team ceiling.
Enable governance by giving Busbar a place to accumulate the ledger (a store) and a cost source
(rate_card). Admin access to the key/group API is gated by the admin auth chain, which serves on
the admin plane (admin_listen, 127.0.0.1:8081 by default — loopback only, isolated from
the :8080 data plane).
# ── the ONE cost source (micro-units per token; absent = token cost derives to 0) ──rate_card: sonnet-anthropic: { input_utok: 3.0, output_utok: 15.0, cache_read_utok: 0.3, cache_write_utok: 3.75 }per_request_fee: 0 # flat per-request charge, in the same unit (default 0)
# ── where the ledger lives (memory is compiled-in; a durable store is a plugin) ──store: module: memory # e.g. module: postgres, settings: { url: "postgres://…" }
# ── the admin auth chain that guards /api/v1/admin/* ──auth: admin_auth: - admin-tokens: { token: { env: BUSBAR_ADMIN_TOKEN } } # boot fails if the ref is unset
# ── the ONE limit tree ──groups: acme: # org ceiling limits: - { requests: 500, per: minute } - { budget: 1000000, per: month } growth: # a team under the org parent: acme limits: - { budget: 200000, per: month } child_default: # stamped onto each user leaf auto-provisioned here limits: - { budget: 2000, per: month }Busbar never writes config.yaml. The groups: tree (and rate_card, per_request_fee,
and the other single-value sections) are all runtime-mutable over the admin API and persisted to
the overlay when BUSBAR_CONFIG_OVERLAY is set. rate_card and per_request_fee are hot-applied
(no restart); a rate correction re-prices history on the next read.
Mint a key bound to a group. The signed token is returned exactly once:
curl -s -X POST http://localhost:8081/api/v1/admin/keys \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN" \ -H "content-type: application/json" \ -d '{"name":"staging-app","group":"growth"}'# → 201 with the signed token, shown once. Store it now.Auto-provision a user leaf in the same call by naming a new group under an existing
parent; the leaf is created with limits stamped from the nearest-ancestor child_default, and
the key is bound to it — the first self-mint materializes a personal budget bucket live in the
enforcement chain:
curl -s -X POST http://localhost:8081/api/v1/admin/keys \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN" \ -H "content-type: application/json" \ -d '{"name":"alice","group":"user:alice","parent":"growth"}'For a Bedrock-SDK client (inbound SigV4 auth), add "issue_aws_credential": true; details under
Mint a key.
The fail-open default is configurable: set the store’s fail-closed posture to reject requests on any store error, a hard budget guarantee for regulated deployments.
Check a key’s attribution any time (the full key lifecycle is under Admin API):
curl -s http://localhost:8081/api/v1/admin/keys/<id>/usage \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN"# → { id, group, spend_cents, tokens, requests, rate_headroom, … }Hierarchical groups & the limit tree
Section titled “Hierarchical groups & the limit tree”The groups: tree is the one place every limit lives. A group is a named enforcement bucket:
an ordered list of generic limits plus an optional parent forming an acyclic chain of any depth.
Every request walks the chain up and enforces every limit of every ancestor (AND,
atomically). A limit is one of four metrics:
budget— a spend cap (inrate_cardunits) over aminute/hour/day/month/totalwindow.requests— a requests-per-window cap (the “requests per minute” limit).tokens— a tokens-per-window cap (the “tokens per minute” limit).concurrent— an in-flight cap (noper; it’s a live gauge, not a window).
A user is a leaf group parented to their team. The personal budget is the leaf’s own limits,
always sub-capped by the team ceiling — because the chain ANDs, per-user budgets can never sum
past the team pool. A child_default on a parent is the per-head template stamped onto each user
leaf auto-provisioned under it (nearest ancestor wins). Setting enabled: false freezes a
group (every request through it is rejected while its history is kept).
A limit can be pool-scoped (pool: <name>), giving that pool its own independent window: two
{ budget: 5000, per: month, pool: … } limits are two separate 5k budgets, one per pool.
Why it matters: “raise Alice’s budget” is one PATCH to her leaf’s limits; “freeze this team”
is enabled: false on the team; org/team/user roll up automatically. No per-key cap juggling.
Define the tree in config.yaml, or mutate it live over the admin API (both are equivalent; the
write verbs accept a groups: block verbatim). All group mutations are served on the admin plane
under /api/v1/admin/groups, audited, and versioned.
groups: acme: # org ceiling limits: - { requests: 500, per: minute } - { budget: 1000000, per: month } growth: # a team under the org parent: acme limits: - { requests: 50, per: minute } - { budget: 200000, per: month } child_default: # per-head budget for user leaves under this team limits: - { budget: 2000, per: month } user:bob: # a USER = a leaf group parented to the team parent: growth enabled: true # false = freeze this group limits: - { requests: 10, per: minute } - { requests: 1000, per: day } - { concurrent: 5 } # no `per` = in-flight capRaise one group’s limits without touching the rest of its definition, or check how a team is tracking against its caps:
# PATCH is a partial update: only the fields you send changecurl -s -X PATCH http://localhost:8081/api/v1/admin/groups/growth \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN" -H "content-type: application/json" \ -d '{"limits":[{"budget":500000,"per":"month"}]}'
curl -s http://localhost:8081/api/v1/admin/groups/growth/usage \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN"A group defined in config.yaml is file-owned: the API answers its mutations with a 409
(the API cannot silently shadow operator file config). Groups created only over the API live in
the overlay and are fully mutable. A delete is refused with a 409 while another group still names
it as parent (re-parent or remove the children first — a delete never orphans them).
Rate limits
Section titled “Rate limits”A group can carry a requests-per-window and a tokens-per-window limit (typically
per: minute). The requests limit is enforced precisely: the counter increments synchronously
on admission. The tokens limit is best-effort under concurrency, since token counts are known only
after the response.
Because limits live on groups and chain, a per-minute cap on a team applies across all the team’s keys at once, and a tighter cap on a user leaf applies to just that user — both enforced in the same walk.
Why it matters: you can rate-limit an internal tool independently of the production path, or keep a noisy batch job from starving interactive traffic — at any level of the tree, without writing limiter code.
Add requests / tokens limits to the relevant group (or PATCH them in later). Omit a metric to
leave it unlimited:
groups: internal-tool: parent: acme limits: - { requests: 120, per: minute } - { tokens: 200000, per: minute }Rate windows are per-process, in-memory. Running multiple Busbar instances against the same store means the requests/tokens windows are per-instance, not shared; budgets (which are store-backed) are shared. For distributed rate limiting, run one instance per store or front the fleet with an upstream rate limiter.
Model access
Section titled “Model access”A key’s allowed_pools list restricts which pools it may target. Omitting it means all
pools; an explicit empty list [] means no pools (the empty set is the empty set, everywhere
in the 1.5.0 config). Access is scoped to pools — the named routing targets you define.
Why it matters: a cheap-staging key can be locked to a low-cost pool; a partner key can be fenced to exactly the pools you intend to expose, with no way to reach the rest.
curl -s -X POST http://localhost:8081/api/v1/admin/keys \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN" \ -H "content-type: application/json" \ -d '{"name":"partner-key","group":"partners","allowed_pools":["public-fast","public-smart"]}'The key now returns 403 if it targets any pool outside that list. allowed_pools can also be
granted by role through an IdP: a role_bindings.<module>.<role>.allowed_pools binding sets the
pools for every principal asserting that role (omitted = all pools).
Admin API
Section titled “Admin API”All governance endpoints live under /api/v1/admin/ on the admin plane (admin_listen,
127.0.0.1:8081 by default — a loopback-only listener, isolated from the :8080 data plane, that
refuses to boot on a network-exposed bind without mTLS). They are gated by the admin auth chain
(auth.admin_auth); present the operator credential as x-admin-token: <token> or
Authorization: Bearer <token>. These are not virtual keys, and they are not the vendor SDK carriers
(x-api-key / x-goog-api-key).
Minting a key requires the mint scope (or full); group and lifecycle mutations require
full; every read is read-only. The delegated mint scope is the credential for a customer’s
self-service portal — it can mint keys (auto-provisioning a user leaf on first mint) but nothing
else.
Mint a key
Section titled “Mint a key”curl -s -X POST http://localhost:8081/api/v1/admin/keys \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "team-ml", "group": "growth", "allowed_pools": ["fast", "overflow"], "expires_in": "30d" }'Response includes id (vk_<16hex>), the signed token (shown once), and the key’s metadata.
Store the token immediately. Body fields: name, group? (binds into the limit chain), parent?
(auto-provisions a new leaf under an existing group), allowed_pools? (omitted = all, [] = none),
labels? (echoed onto metric series, never interpreted by enforcement),
expires_in/expires_at? (default 90 days), and issue_aws_credential?.
To also issue an AWS credential pair for Bedrock-SDK clients, add "issue_aws_credential": true. The
201 response then additionally includes aws_access_key_id and aws_secret_access_key, both shown
once. Busbar verifies the inbound SigV4 signature and enforces the key’s governance controls. See
Bedrock ingress.
POST /keys accepts an Idempotency-Key header: a retried request with the same key inside the
~10-minute window returns the first response verbatim (including the once-shown token) instead of
double-minting.
List keys
Section titled “List keys”curl -s "http://localhost:8081/api/v1/admin/keys?group=growth" \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN"Returns key metadata ({id, name, allowed_pools, group, enabled, created_at, labels}), id-sorted and
cursor-paginated. Filters: ?enabled=, ?prefix=vk_ab, ?group=. The token and its hash are never
returned.
Check usage
Section titled “Check usage”curl -s "http://localhost:8081/api/v1/admin/keys/<id>/usage" \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN"Returns the key’s all-time attribution counters {id, group, spend_cents, tokens, requests, rate_headroom, …}. spend_cents is derived at read time from the token ledger × the current
rate_card plus fee (nothing dollar-shaped is stored). The key’s limits, if any, live on its bound
group; read those with GET /api/v1/admin/groups/<name>/usage. 404 if the key does not exist.
Update a key
Section titled “Update a key”curl -s -X PATCH "http://localhost:8081/api/v1/admin/keys/<id>" \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }'PATCH adjusts enabled and/or group (three-state: absent = unchanged, null = unbind to
authed-but-unlimited, a value = rebind to an existing group). Limits are not set on a key — move
them on the group. Honors If-Match. 404 if the key does not exist.
Rotate a key
Section titled “Rotate a key”curl -s -X POST "http://localhost:8081/api/v1/admin/keys/<id>/rotate" \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN"Mints a fresh token in place: same id, so budgets, rate windows, usage history, and audit
attribution carry over; the old token stops resolving immediately; the new one is shown exactly once.
Revoke a key
Section titled “Revoke a key”curl -s -X DELETE "http://localhost:8081/api/v1/admin/keys/<id>" \ -H "x-admin-token: $BUSBAR_ADMIN_TOKEN"Returns 204 No Content; the key stops resolving immediately. 404 for an unknown id. Honors
If-Match.
Enforcement model and precision
Section titled “Enforcement model and precision”Understanding where each limit is precise and where it is approximate matters for setting realistic caps:
Requests: precise. The counter is incremented synchronously on admission, before the request is forwarded. A request that would exceed the limit is rejected before any upstream call is made.
Tokens: best-effort. Token counts are recorded post-response, after the upstream reports them. In-flight concurrent requests do not yet contribute to the window. If multiple large concurrent requests arrive simultaneously at the window boundary, all may be admitted before the tokens limit kicks in.
Budget: atomic hard cap. The over-budget check and the spend charge are performed under a single critical section across the whole group chain (check every bucket, then charge every bucket). Concurrent requests cannot cause an overshoot; if the cap is reached mid-burst, the excess requests are rejected. On a store error during the admission check, the default fails open (preserves availability); the fail-closed posture rejects on any store error. A definitive over-budget result always rejects regardless. The per-request fee is charged at admission on the key’s own bucket and refunded on a non-2xx outcome (the admission slot itself is never refunded).
Concurrent: precise. A live per-group gauge, incremented on admission and released when the request completes; over-cap requests are rejected before dispatch.
allowed_pools ACL: precise and pre-forwarding. A key with allowed_pools: ["fast"] trying to
target overflow or any direct model route not in the list gets 403 before any upstream call.
Requests/tokens/concurrent windows are per-process, in-memory; budget accrual is store-backed and shared across a fleet pointed at the same store. For distributed request/token rate limiting, run one instance per store or front the fleet with an upstream rate limiter.