Plans and pricing
A plan is the smallest billable unit: a price + a billing period + a set of quotas + a tier. Every active service has one or more plans; subscriptions reference plans by ID.
Schema#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
Tier conventions#
The four canonical tier values map to standard product expectations:
| Tier | Use |
|---|---|
trial |
14-day free trial. base_price_cents = 0, trial_days = 14, low quotas. |
starter |
Entry paid tier. Small quotas, basic features. |
pro |
Mainstream paid tier. The "sweet spot" most customers land on. |
enterprise |
Top tier. Often custom-quoted; the plan in the DB is a reference price. |
Tier names are free-form strings — these are just the conventions ScaiLabs' own services use.
Quotas#
plans.quotas is service-specific JSON. Examples:
1 2 3 4 5 6 7 8 9 10 11 | |
A quota value of 0 (or absent) is the conventional "unlimited" sentinel for Enterprise tiers.
Quotas are enforced by each service itself, not by ScaiControl. ScaiControl publishes them; the service polls (or webhooks back) for the current plan_id and reads its own quota.
Pricing normalisation#
For events and reports, prices are normalised to monthly (MRR):
billing_period |
MRR formula |
|---|---|
monthly |
base_price_cents |
yearly |
base_price_cents / 12 |
quarterly |
base_price_cents / 3 |
weekly |
base_price_cents * 4 |
daily |
base_price_cents * 30 |
one_time |
base_price_cents (pass-through; consumer interprets) |
services/events/builders.py:_normalize_mrr() is the canonical implementation; subscription event payloads (subscription.activated.v1 etc.) carry the normalised value as mrr_amount_cents.
Plan keys for stable referencing#
Plan IDs are UUIDs, which churn between re-seeds. For event payloads and cross-system references, ScaiControl emits a composite plan_key of the form:
1 | |
Examples: scaikey.starter, scaivault.pro, scaiwave.enterprise. The plan_key is stable across re-seeds (assuming you keep slug conventions consistent), while plan_id is still emitted alongside for the database-level link.
Usage-based pricing#
For metered services (API calls, storage GB, model invocations), plan_usage_pricing adds tiered overage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
The monthly billing job (services/billing/invoice.py:generate_monthly_invoices) joins this with the metering data (usage_events, partitioned by month) to compute overage lines.
Editing plans without orphaning subscriptions#
Best practice:
- Never delete a plan that has live subscriptions. Make it
is_active = falseandis_public = falseso it stops appearing in the catalog, but existing subscriptions keep working. - Price changes only take effect on the next billing cycle. Already-finalised invoices snapshot their VAT but not their plan price (the line item carries the exact
unit_price_cents). So an active subscription whose plan price changed mid-month bills the new price on the next cycle. - Trial-day changes apply only to newly-created subscriptions. An in-flight
trialingsubscription keeps its originaltrial_ends_at.
Admin surface#
- List plans for a service:
GET /catalog/services/{slug}/plans(also returned inGET /admin/registry/serviceswith?expand=plans). - Create/edit plans:
POST/PUT /admin/registry/services/{slug}/plans(see Admin — service registry). - The admin UI exposes plan management on each service's detail page.
See also#
- Service packs for bundled
(service, plan)combinations. - Subscriptions for the lifecycle of "tenant on a plan".
- VAT and reverse charge for how prices flow into invoices.