Skip to main content

Billing Service — Overview

The Billing Service is the subscription and usage-limits engine for the platform. It is an optional service — the platform runs fully without it. When not deployed (e.g. self-hosted or behind a private license model), the API Gateway skips all limit checks and subscription status validation, allowing all tenant requests through.

Zero-state guarantee: When a new tenant is created, the Billing Service automatically creates a subscription in trialing status. There is no ambiguous "no subscription" state — every tenant always has an active subscription record.


Technical Stack

ComponentTechnologyDetail
LanguageGopgxpool + pgx/v5 + sqlc (sql_package: pgx/v5)
DatabasePostgreSQLPlans, subscriptions, usage, webhook events
Amountsshopspring/decimalAll monetary amounts stored in cents
Payment providerStripe (stripe/stripe-go)Or custom internal engine
CacheValkeyplan_info:{tenantId} — subscription status + limits, TTL 15 min
EventsKafkaPublishes billing.subscription.changed, billing.plan.changed

Internal Packages

PackageResponsibility
internal/configEnvironment-based configuration
internal/handlerHTTP request handlers
internal/modelDomain models: Plan, Subscription, Usage
internal/producerKafka event producer
internal/repositoryPostgreSQL repositories (sqlc-generated)
internal/serviceBusiness logic: plan changes, lifecycle, limit enforcement
internal/webhookStripe Stripe-Signature verification (stripe.ConstructEvent())
internal/workerBackground workers: escalation timeline, trial expiry

gRPC Service: 14 RPCs

All billing operations are exposed as gRPC RPCs, consumed by the API Gateway and internal services. The Gateway translates REST calls to these RPCs.

GroupRPCs
Plan CRUDCreatePlan · RetrievePlan · ListPlans · UpdatePlan
Subscription lifecycleCreateSubscription · RetrieveSubscription · ListSubscriptions · UpdateSubscription · CancelSubscription · CancelDowngrade
Usage & limits (hot path)RetrieveUsage · CheckLimit
Webhook processingHandleWebhook

CheckLimit is the critical hot-path RPC. It is called by the Gateway on every mutating request when Billing is active. Its response is cached in Valkey — the gRPC call is only made on a cache miss (see Usage Limits).


Subscription Lifecycle Summary

Tenant created → trialing (automatic, zero-state)

├─ Payment method added → active
├─ Trial expires, no payment method → past_due → escalation timeline

└─ active

├─ Payment success → active (continues)
├─ Payment failure → past_due → (7d) → suspended → (30d) → terminated
└─ Manual cancel → canceled

Status definitions:

StatusAccessNotes
trialingFullAuto-created on tenant registration. No payment method required.
activeFullSubscription in good standing.
past_dueFull (grace period)Payment failed. Tenant continues with full access for up to 7 days.
suspendedRead-only8–37 days past due. POST/PATCH/DELETE blocked (except Money operations).
terminatedNone38+ days past due. All requests blocked. Async soft-delete starts.
canceledNoneManually canceled.

Billing Permissions

PermissionWhoDescription
billing.plan.changeOwner, AdminChange subscription plan (financial impact)
billing.payment.manageOwner, AdminAdd/remove payment methods
billing.info.updateOwner, AdminUpdate billing email, address
billing.viewAll authenticatedView invoices and usage (read-only)
billing.exportOwner, AdminExport billing data

REST API (via Gateway)

MethodEndpointDescription
GET/api/v1/billing/plansList available plans
GET/api/v1/billing/plans/:idGet plan details
GET/api/v1/billing/subscriptions/currentGet current tenant subscription
PATCH/api/v1/billing/subscriptions/:idChange plan (requires version for optimistic locking)
DELETE/api/v1/billing/subscriptions/:idCancel subscription
POST/api/v1/billing/subscriptions/:id/cancel-downgradeCancel a pending downgrade
GET/api/v1/billing/usageCurrent usage vs limits
POST/api/v1/billing/webhooks/{provider}Receive webhook from payment provider

Optional Billing: Self-Hosted Mode

When the Billing Service is not deployed:

Gateway limit check:
→ Is BILLING_SERVICE_ENABLED=false?
→ Yes → skip plan_info:{tenantId} lookup
skip subscription status check
skip limit enforcement
forward request to upstream directly

All tenants have unlimited access to all platform features. There are no subscription records. The platform behaves as an unrestricted engine. This is the default behavior for self-hosted deployments that do not require monetization.


  • Subscriptions — lifecycle, plan change state machine, optimistic locking, downgrade grace period
  • Usage Limits — Gateway auto-check, Valkey cache, limit types, 402 Payment Required, FinCEN Money exemption
  • Escalation — grace period, suspension, termination, async soft-delete, instant reactivation on payment
  • Webhooks — Stripe webhook processing, signature validation, idempotency, retry policy