Billing types
Billing operates at the business level. Each business row carries
billingMode (how they pay) and billingStatus (current standing).
These are PostgreSQL enums defined in apps/scheduler-api/src/db/schema.ts.
Billing modes
Section titled “Billing modes”| Mode | Description | Default |
|---|---|---|
none |
No platform billing configured yet | ✓ |
a_la_carte |
Per-usage charges — billed as services are consumed | |
contract |
Fixed-rate contract (monthly or annual) |
The mode is set at provisioning time and can be changed by an admin.
Collection method (contract mode only)
Section titled “Collection method (contract mode only)”When billingMode = 'contract', a collectionMethod field controls how Stripe collects:
| Method | How payment is collected |
|---|---|
subscription |
Recurring Stripe subscription (auto-charged) |
prepaid |
Single upfront charge for the contract period |
external |
Invoiced outside Stripe (e.g. wire transfer, net-terms) |
Contract expiry
Section titled “Contract expiry”Contracts have a contractEndsAt timestamp. When a contract term ends, the
contractExpiryAction determines what happens:
| Action | Effect |
|---|---|
restrict |
Access curtailed — business moves to restricted billing status |
auto_convert |
Automatically flips billingMode to a_la_carte |
comp_continue |
Access continues at no charge (complimentary extension) |
Billing status
Section titled “Billing status”| Status | Meaning |
|---|---|
active |
In good standing; all features available |
past_due |
Payment overdue; within the grace period (pastDueGraceDays, default 7 days) |
restricted |
Access curtailed — grace period expired or contract restriction triggered |
Default status is active.
Status state machine
Section titled “Status state machine”stateDiagram-v2
[*] --> active : provisioned
active --> past_due : Stripe payment fails
past_due --> active : payment retried successfully
past_due --> restricted : grace period expires
active --> restricted : contract ends with restrict action
restricted --> active : admin re-activates / payment resolved
active --> active : contract ends with auto_convert\n(billingMode flips to a_la_carte)
active --> active : contract ends with comp_continue\n(access unchanged)
How billing status updates
Section titled “How billing status updates”- Stripe webhooks — payment events (invoice.paid, invoice.payment_failed) trigger immediate status updates.
- Billing scanner — a cron job finds overdue businesses, attempts retry charges via Stripe, and transitions status on success or grace-period expiry.
- Both paths mirror results into
billing_invoice_mirrorand append tobusiness_billing_audit.
Money gate
Section titled “Money gate”Live, money-moving actions (domain purchases) are gated by a runtime toggle
in the platform_config table — not an env var:
| Config key | Default | Effect when false |
|---|---|---|
domains_purchase_enabled |
false |
Domain purchases fail with a clear error; no Porkbun call is made |
The money gate is toggled by an admin in the portal. It takes effect immediately without a deploy.
Stripe entities
Section titled “Stripe entities”Businesses on a_la_carte or contract (subscription/prepaid collection) have:
- Stripe Customer — linked via
business.stripeCustomerId - Stripe Subscription (subscription collection) — tracked in
business.stripeSubscriptionId - Invoice mirror — local cache in
billing_invoice_mirrortable, upserted by webhook on every invoice event - Audit log — every billing event written to
business_billing_audit
Businesses with billingMode = 'none' or collectionMethod = 'external' have a Stripe Customer but no active Stripe Subscription.
Catalog sync
Section titled “Catalog sync”Stripe products and prices are synced from code on boot via
modules/billing/stripe-catalog-sync.ts. The catalog is defined in code; Stripe
is kept in sync, not the other way around.
Related
Section titled “Related”- Business billing flow — runtime sequence diagram
- Admin: platform config — how to toggle the money gate
- Concepts: Billing — architecture + decision log links