Skip to content

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.

Requestsigned virtual keyGovernance gateallowed_pools ACLgroup budget cap (chain)rate limit · requests/tokensadmittedPool→ upstream403 / 429any check fails → denied

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.

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 (in rate_card units) over a minute / hour / day / month / total window.
  • 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 (no per; 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.

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.

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.


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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

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.