Go API
Install the versioned module:
go get github.com/Zonastery/bursar/golang/v2
The Go SDK mirrors Bursar's Python and TypeScript contracts with
context.Context-first operations and typed (T, error) results.
:::important Python owns the CLI
The Go module deliberately has no CLI, migration command, or tenant
administration command. Use the Python bursar CLI to apply the shared SQL
baseline and administer tenants, then use Go for application runtime behavior.
See the quickstart.
:::
Package map
| Area | Public Go surface |
|---|---|
| Facade and runtime | bursar.New, Bursar, lightweight NewBursarRuntime, and storage/runtime.NewBursarRuntime |
| Catalog and pricing | CatalogService, PricingEngine, UsageMetrics |
| Credits and accounting | CreditsService, exact Amount, priced usage, leases, plans, quotas, teams, ledger, and analytics |
| Billing and commerce | BillingService, normalized lifecycle events, customer/subscription/invoice/payment state, checkout, auto-recharge, and CommerceService |
| Payment providers | providers/stripe, providers/dodo, providers/mock, and ProviderRegistry |
| Google ADK (optional) | Separate golang/integrations/googleadk/v2 module with replay-safe model-call admission, settlement, and durable session recovery |
| Storage and outbox | PostgresStorageRepository, OutboxWorker, projection handlers, maintenance, storage/clickhouse, and storage/s3 |
| Telemetry and resilience | Instrumentation, telemetry/opentelemetry, and RetryBursarOperation |
The complete exported signatures are available on pkg.go.dev.
Google ADK users install
github.com/Zonastery/bursar/golang/integrations/googleadk/v2 separately and
register its plugin through ADK's runner.PluginConfig. That module follows
ADK's Go 1.26.5 requirement; the core SDK retains its Go 1.25 floor. See
Meter Google ADK model calls for the complete
registration and financial-lifecycle example.
Construct the facade and runtime
For a complete process runtime, use the storage composition root. It accepts database URLs or caller-owned pools and keeps the tenant-bound primary and unscoped operator connections distinct:
package main
import (
"context"
"log"
"os"
bursar "github.com/Zonastery/bursar/golang/v2"
storageRuntime "github.com/Zonastery/bursar/golang/v2/storage/runtime"
)
func main() {
ctx := context.Background()
runtime, err := storageRuntime.NewBursarRuntime(ctx, storageRuntime.Options{
TenantID: os.Getenv("BURSAR_TENANT_ID"), // UUID
TenantSlug: os.Getenv("BURSAR_TENANT_SLUG"),
ProviderEnvironment: bursar.ProviderEnvironmentTest,
DatabaseURL: os.Getenv("DATABASE_URL"),
OperatorDatabaseURL: os.Getenv("BURSAR_OPERATOR_DATABASE_URL"),
})
if err != nil {
log.Fatal(err)
}
if err := runtime.Start(ctx); err != nil { // Verifies identity and retries catalog loading.
log.Fatal(err)
}
defer runtime.Close(context.Background())
if !runtime.Health(ctx).Ready {
log.Fatal("Bursar runtime is not ready")
}
}
storage/runtime.NewBursarRuntime validates the tenant UUID and optional slug,
constructs the facade, recovery and maintenance handles, optional
ClickHouse/S3 projections, and the outbox worker. Its Start, Health,
State, CheckDependencies, Flush, and Close methods own that lifecycle.
Health and State are race-safe local snapshots and never perform network or
database I/O. Use CheckDependencies when a probe should actively verify
PostgreSQL, the loaded catalog revision, bounded outbox status, dead letters,
and configured component health.
The operator connection is deliberately unscoped and separate from the
tenant-bound primary connection.
The root bursar.NewBursarRuntime is the lightweight alternative. It accepts
an already constructed *bursar.Bursar plus optional RuntimeComponent
instances and coordinates catalog loading, component lifecycle, readiness,
dependency checks, flush, and close; it does not create stores, projections,
or workers. Both runtimes leave migrations and CLI administration to Python.
Bursar exposes Credits, Catalog, Accounts, and optional Billing and
Commerce capabilities.
Exact and metric-priced credits
All credits, measures, and prices use bursar.Amount, an alias of the
battle-tested shopspring/decimal
type. Parse runtime input with NewAmount; use MustAmount only for static
constants and tests. QuantizeMoney applies the shared six-decimal, half-up
accounting rule.
metrics := bursar.UsageMetrics{
Operation: "completion",
Measures: map[string]bursar.Amount{
"input_tokens": bursar.MustAmount("1200"),
"output_tokens": bursar.MustAmount("300"),
},
Dimensions: map[string]any{"model": "gpt-5"},
}
charged, err := sdk.Credits.DeductUsage(ctx, userID, metrics, bursar.PricedUsageOptions{
IdempotencyKey: "request:job-42",
})
DeductUsage, CanAffordUsage, RecordUsageMetrics, and DeductTeamUsage
price UsageMetrics with the subject's effective catalog and rate card.
ReserveUsage records the pricing context on a durable lease;
SettleUsage prices final metrics against that immutable lease revision, so a
catalog activation cannot change an in-flight charge.
For work whose final metrics are only known after execution,
CreditsService.RunBilledUsage combines a metric-priced reserve, work
callback, and settlement:
result, err := sdk.Credits.RunBilledUsage(ctx, userID, bursar.RunBilledUsageOptions{
Estimate: estimatedMetrics,
OperationKey: "request:job-42",
DoWork: func(ctx context.Context) (any, bursar.UsageMetrics, error) {
value, metrics, err := runModel(ctx)
return value, metrics, err
},
})
The operation key is reused for replay-safe settlement. A work failure
releases the lease; retryable settlement reuses the same idempotency context.
CreditsServiceOptions.CatalogCacheTTL controls TTL-aware lazy catalog
refresh (five minutes by default; zero disables automatic refresh), and
LazyExpiry enables expiry handling during normal credit reads.
LowBalanceConfig adds edge-triggered CreditEventHandler notifications via
its Thresholds, OnTrigger, and bounded MaxTrackedUsers fields. The
concise LowBalance thresholds remain available without a callback.
The lower-level exact-amount methods remain available for administrative or
already-priced operations: AddCredits, Deduct, Reserve, Settle,
Release, Renew, refunds, grants, plans, entitlements, quotas, teams,
ledger pages, usage receipts, and spend aggregates.
Billing management and provider events
BillingService owns durable event claim/complete/fail processing and the
default provider-neutral PostgreSQL lifecycle. Its management methods cover
checkout intents, customer and subscription state, preferences, invoices,
subscription changes, offer/top-up resolution, grace expiry, and
pseudonymization. When the store supports auto-recharge, the facade also
attaches Billing.AutoRecharge for profile, quote, enable/disable, retry, and
post-deduction processing. CommerceService adds checkout, verified webhook,
subscription commands, plan changes, portals, invoice links, and account
overview operations.
Register post-completion billing notifications with
BillingServiceOptions.EventHandlers (or BillingService.OnEvent). These
BillingEventCallback functions are failure-isolated and cannot change a
completed event. Use BillingServiceOptions.Handlers, On, or
SetDefaultHandler only when deliberately replacing a normalized lifecycle
route. CommerceService.GetAccountOverview(ctx, accountID) returns the
combined AccountCommerceOverview projection, including durable credit,
entitlement, subscription, invoice, transaction, usage, payment-method, and
availability sections.
Provider-specific accounting inputs can be captured through
ProviderReceiptSource, whose Begin/Finish pair returns a
*ProviderReceipt containing exact UsageMetrics and financial metadata.
ProviderReceipt.Validate checks that the metrics can be priced. This keeps
provider response parsing at the integration boundary while settlement
continues through the normal credits API.
Provider adapters verify raw webhook signatures before returning a normalized
BillingEvent. Only then should the application ingest the event:
webhook, err := bursar.WebhookRequestFromHTTP(request, 1<<20)
if err != nil {
return err
}
verified, err := provider.HandleWebhook(request.Context(), webhook)
if err != nil {
return err
}
if verified.Event != nil {
_, err = sdk.IngestBillingEvent(request.Context(), *verified.Event)
}
return err
The maintained adapters are:
providers/stripe.New, backed by the officialstripe-goclient.providers/dodo.New, backed by Dodo's official Go client and Standard Webhooks verification.providers/mock.New, a deterministic in-memory implementation for tests and local examples only.
Stripe and Dodo implement the portable checkout and webhook contract plus
their supported optional capabilities, including checkout status, customer
and payment-method portals, customer creation, subscriptions, invoices,
saved-payment charges, and plan changes. ProviderRegistry lazily constructs
configured providers once. When a registry is attached through
CommerceOptions, the facade rejects a live, test, or sandbox environment that
does not match the tenant-bound store.
Storage projections, outbox, and maintenance
PostgreSQL remains authoritative for accounting and the transactional outbox.
NewPostgresStorageRepositoryFromStore exposes leased claim renewal,
completion, retry/dead-letter recovery, stats, canonical usage exports, and
billing-payload exports.
repository, err := bursar.NewPostgresStorageRepositoryFromStore(store)
if err != nil {
return err
}
usageHandler, err := bursar.NewUsageChargeOutboxHandler(repository, usageSink)
if err != nil {
return err
}
archiveHandler, err := bursar.NewBillingPayloadOutboxHandler(repository, billingArchive)
if err != nil {
return err
}
worker, err := bursar.NewOutboxWorker(
repository,
[]bursar.OutboxHandler{usageHandler, archiveHandler},
bursar.OutboxWorkerOptions{Concurrency: 4},
)
OutboxWorker is a bounded, concurrent RuntimeComponent with claim
heartbeats, exponential retry delays, single-flight flushes, claim-loss
reporting, and dead-letter limits. Snapshot exposes local lifecycle, the last
manual run, and the last sanitized worker error without touching the store.
Stop idempotently stops polling while leaving the caller-owned store open;
Close provides the final RuntimeComponent shutdown alias. The two supplied
handlers project only canonical records reloaded from PostgreSQL when
necessary.
UsageChargeOutboxHandler detects the optional BatchUsageEventSink
capability. With a batch sink, concurrent deliveries coalesce over a small
fixed window into one WriteUsageBatch call; every handler waits for the
shared result. storage/clickhouse.ClickHouseUsageStore implements this
interface and preserves each UsageExportEntry.OutboxEventID, so retries and
replays remain idempotent. Sinks that implement only UsageEventSink retain
the direct one-event WriteUsage path, and cancellation still reaches the
underlying context.
Optional projection packages keep PostgreSQL as the financial authority:
storage/clickhouse.NewUsageStoreuses the officialclickhouse-go/v2connection for idempotent usage projection, schema compatibility checks, receipt history, and spend analytics.storage/s3.NewBillingArchiveuses the official AWS SDK for Go v2 to archive canonical billing envelopes under deterministic tenant-scoped keys. It supports the AWS credential chain, S3-compatible endpoints, server-side encryption, and checksums.
NewBursarMaintenanceFromStore runs bounded tenant-scoped lease, credit,
catalog-change, and optional billing-grace work when invoked by the host.
NewBursarOperatorMaintenance is restricted to an unscoped
bursar_operator PostgreSQL client for storage-retention and partition work.
Neither helper schedules itself.
Telemetry and retry
Core instrumentation is vendor-neutral and no-op by default. To use the
official
telemetry/opentelemetry
Go API adapter, configure the host's SDK/exporters and enable it before
constructing stores and services:
import bursarotel "github.com/Zonastery/bursar/golang/v2/telemetry/opentelemetry"
restore, err := bursarotel.Enable(bursarotel.Options{})
if err != nil {
return err
}
defer restore()
The adapter emits operation spans, counts, and durations through host-provided tracer and meter providers. Bursar allowlists and sanitizes low-cardinality attributes; raw SQL, provider payloads, secrets, emails, and full identifiers are excluded.
RetryBursarOperation uses bounded, context-aware exponential backoff from
cenkalti/backoff/v5. By default it makes at most three attempts and retries
only typed Bursar errors marked retryable. Retry mutations only when they are
replay-safe and always reuse the exact idempotency key.
result, err := bursar.RetryBursarOperation(ctx, func(ctx context.Context) (bursar.BalanceResult, error) {
return sdk.Credits.GetBalance(ctx, userID)
})
Errors
SDK failures are typed *bursar.BursarError values. Use Go's normal
errors.As/errors.Is flow or bursar.AsBursarError; stable codes,
categories, retryability, and the indeterminate-write flag let transport
adapters make safe decisions without parsing messages.
if classified, ok := bursar.AsBursarError(err); ok && classified.Retryable {
// Retry only a read or a replay-safe mutation with its original key.
}
The Go package contains no router integration or CLI. Applications retain
their own net/http, Chi, Gin, Fiber, gRPC, worker, and scheduler composition.