Billing & Entitlements
Epic 13 replaced ad-hoc tier checks with a single entitlement system: a database catalog of limits, quotas, and features, resolved per billing account, enforced in the database, and translated into one canonical error envelope for the UI.
Mental model
Three rules explain almost every design decision in this section:
- The catalog is the single source of numbers. Every advertised and enforced limit lives in
entitlement_catalog.plan_defaults(seeded in migration20260608110521§10, extended by later migrations). The public pricing page hydrates from the same table (buildPricingViewModelinsrc/lib/billing/pricing-display.ts, loaded bysrc/routes/pricing/+page.server.ts), so an advertised number and an enforced cap can never diverge. Dollar prices are the only exception: they live in code (pricing-display.tsfor display, Stripelookup_keys viasrc/lib/billing/price-catalog.tsfor checkout), never in the DB. - Resolution is per account, never per plan. The effective cap for
(account, key)is computed by the SQLeffective_cap()family: the catalog default (or an absolute per-account override), plus active additive override deltas, plus the remaining one-time add-on pool. Code must never infer a cap fromplan_key; grandfathering, admin comps, Enterprise negotiated caps, and add-on packs all make a real account differ from its plan defaults. - Fail closed (the C8 rule). A missing
(account, key)row inaccount_entitlementsis a DENY, distinct from a row whose cap resolves toNULL(intentionally unlimited). Enforcement lives in the database (BEFORE triggers andSECURITY DEFINERRPCs), so direct PostgREST writes hit the same walls as app code. App layers only pre-check for UX and translate DB raises into friendly errors.
Plans
Plan keys are defined in src/lib/billing/plan-mapping.ts and stored on billing_accounts.plan_key.
plan_key | Display name | PLAN_ORDER rank | Checkout | Notes |
|---|---|---|---|---|
free | Free | 0 | None (signup default) | Bootstrap plan for every new account |
creator | Creator | 1 | Self-serve | |
studio | Studio | 2 | Self-serve | |
founders | Founders | 2 | Self-serve (one-time offer) | Resolves entitlements as Studio (see below) |
production_house | Production House | 3 | Self-serve | |
enterprise | Enterprise | 4 | Sales-led (invoiced, no self-serve) | plan_key written only by provision_enterprise_account |
SELF_SERVE_PLAN_KEYS = ['creator', 'studio', 'production_house', 'founders']. Free has no checkout; Enterprise is provisioned manually against asend_invoicesubscription.- Founders → Studio remap: the catalog seeds no
founderskey. Feature baselines remap in TypeScript (remapPlanKeyinsrc/lib/entitlements/core.ts); numeric caps remap insideentitlement_base_cap()in SQL. Founders ranks with Studio inPLAN_ORDERfor upgrade/downgrade direction. - Legacy tier remap (
legacyTierToPlanKey):free → free,starter → creator,professional → studio,enterprise → enterprise, unknown/NULL→ free. Only used for grandfathered Stripe artifacts (oldprice.metadata.tiervalues and the legacy checkout body); theuser_profiles.subscription_tiergate was decommissioned in GATE-3 (migration20260611164202).
Display pricing
From PRICES in pricing-display.ts (display only; checkout resolves live Stripe prices by lookup_key):
| Plan | Monthly | Annual (per month) | Annual (per year) |
|---|---|---|---|
| Creator | $24 | $19 | $230 |
| Studio | $59 | $47 | $566 |
| Production House | $149 | $119 | $1,430 |
| Enterprise | from $499, custom ("Tailored for you", contact sales) | ||
| Founders | $399 one-time, 3 years of Studio-equivalent access, then $49/mo grandfathered (vs $59 Studio list) |
The Founders "100 early adopters" seat count is marketing copy (FOUNDERS_DISPLAY.seats); there is no code enforcement of that cap.
Pages in this section
Recommended reading order:
- Entitlement Model: the catalog,
effective_cap(),consume_quota(), the feature-grant union, the full limits/quotas/features matrices, and add-on packs. - Slot Models & Enforcement Walls: podcast slots, managed-episode slots (PT429), the recording window (PT422), the publish meter (PT430), external hosting (PT403), the storage cap, and starter-content slot semantics.
- Gate Errors & UI Surfaces: the
GateErrorenvelope, the surface mapper (upsell dialog vs inline vs toast), usage meters, and click-time gating. - Provisioning: free bootstrap, webhook-only Stripe grants, Founders and Enterprise provisioning, admin comps, and grandfathering.