Billing on canonical state.
UsageEvent is append-only — the source of truth, never edited. UsageMeterRollup aggregates periodically — quotas reference rollups, not mutable counters. ProjectQuota declares hard / soft / monitor enforcement. Scope-chain billing attribution: a child's usage can be billed to a parent through BillingAttributionProjectId. Same canonical state runs the runtime and the bill.
Ledger first. Counters second. Source of truth never mutates.
Append-only UsageEvent
Every metered event lands as a row. Reads, writes, operation executions, event deliveries, surface installations — all logged. Never edited. Never deleted in normal flow.
Periodic rollups
UsageRollupBackgroundService aggregates UsageEvent into UsageMeterRollup grouped by (ProjectId, MeterKind, BillingScopeId, hourly period). Without rollups, quota enforcement sees zero usage — by design.
Three enforcement modes
Hard: throws QuotaExceededException → HTTP 429. Soft: warning, proceeds. Monitor: no-op, observed only. Configurable per project, per meter kind.
Scope-chain attribution
BillingAttributionProjectId routes a child's usage to a parent. Surface installations attribute to whichever scope you configure. A platform tenant can shoulder a customer's burst.
TTL + bus invalidation
QuotaEnforcementService caches definitions and rollups (30s / 15s TTL) plus subscribes to ICacheInvalidationBus on QuotaEnforcement scope. Mutations invalidate immediately; cache survives bus loss as fallback.
Missing quota = unlimited
By design. A project without an explicit quota row has no limit. Quotas are an opt-in correctness mechanism, not a default trap.
Never edited
Hard · Soft · Monitor
Child → parent → tenant
Typed QuotaExceededException
Bill from the same state that runs your product.
No second metering pipeline. No reconciliation drift. The ledger is the authority. Rollups are the projection. Quotas reference rollups. Enforcement is fail-closed.