Billing & Metering
Purpose
Billing & Metering enforces quota limits and accrues usage metrics for each tenant. It is designed to be generic (swappable billing backend), event-driven, and non-blocking on the hot commit path — except for the synchronous quota gate at upload initiation.
Data owned
| Table | Purpose |
|---|---|
usage_meters |
Per-tenant counters: bytes stored, bytes transferred, file count |
quotas |
Per-tenant limits: bytes_limit, bytes_used |
plans |
Plan definitions: storage limit, transfer limit, feature flags |
The QUOTA row is the authoritative usage gate: bytes_used is maintained by the Meter worker; bytes_limit is set from the plans table on plan change.
Internal API
Billing.* gRPC methods:
| Method | Description |
|---|---|
Billing.CheckQuota |
Synchronous gate: is bytes_used + requested_bytes ≤ bytes_limit? |
Billing.RecordUsage |
Record a usage event (async; called by the Meter worker) |
Billing.GetUsage |
Return current usage summary for a tenant |
Billing.SetQuota |
Update quota limits (admin / plan change) |
Quota enforcement
Quota is checked synchronously at upload initiation — before a presigned URL is issued.
If the check fails (quota exceeded), the upload is rejected with a 429 Quota Exceeded response before any bytes are transferred.
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant BI as Billing
participant F as File & Metadata
C->>GW: POST /v1/files (init upload: size)
GW->>BI: CheckQuota(tenant_id, requested_bytes)
alt quota available
BI-->>GW: allow
GW->>F: CreateUpload(...)
F-->>GW: uploadId + presigned URL
GW-->>C: 201 {uploadId, url}
else quota exceeded
BI-->>GW: deny (bytes_used + requested > bytes_limit)
GW-->>C: 429 Quota Exceeded
end
Usage accrual
Usage accrual is asynchronous — it does not block the commit path:
- The Meter worker consumes
BlobCommittedevents from NATS JetStream. - On each event it increments
QUOTA.bytes_usedfor the tenant. QUOTA.bytes_usedreflects committed bytes only; staging blobs that were never committed do not count.- Deletes are handled via
BlobOrphanedevents: when a blob’s refcount reaches zero and it is GC’d,bytes_usedis decremented.
Plans and limits
| Field | Description |
|---|---|
bytes_limit |
Maximum bytes stored (enforced synchronously at upload init) |
bytes_used |
Current bytes in committed blobs (updated asynchronously) |
file_count_limit |
Maximum number of nodes (enforced at CreateUpload) |
transfer_limit |
Monthly outbound bytes (informational; enforced at billing cycle) |
Plans are stored per tenant in the plans table and are applied at subscription / plan-change time. Changing a plan updates QUOTA.bytes_limit without requiring a deployment.