Pricing Cents Cutover β Execution Runbook
Executes the creditsβcents denomination switch designed in
dollar-balance-migration.md and built across PRs
#1246β#1255. After this cutover, credit_balances.balance (and the team pools)
are denominated in integer cents; price = product_price_cents(product, plan) Γ quantity.
β οΈ This is a non-backward-compatible, irreversible cutover. Migration 185 multiplies every balance Γ100 (no down-migration), migration 187 drops the old 5-arg
deduct_team_creditssignature, and the new code reads balances as cents while the old code reads them as credits. The migrations and the deployed code must land together β applying migrations under the old code (or deploying new code before the migrations) breaks live billing. Do this in a short maintenance window.
0. Pre-flight (validated 2026-06-21)
- Prod data is small and overflow-safe. 78
credit_balancesrows (max balance 8,243 β Γ100 = 824,300, far under the int4 ceiling of 2,147,483,647), 4,201credit_transactions(max |amount| 9,999),teams= 1 row (balance 0),team_members= 0. The Γ100 backfill is fast and lock-light. - Migrations validated individually and 182 end-to-end against the real prod
schema via
BEGIN β¦ ROLLBACK(prices: pdf prepay 100Β’ / team 50Β’ / pro 50Β’ / individual 75Β’; org_chart prepay 2000Β’; worksheet free; unknown pair β NULL fail-closed). The committedsupabase/tests/*.test.sqlsuites cover each. - Known caveat: the full repo migration history does not replay cleanly on
a fresh Supabase branch (branch came up
MIGRATIONS_FAILEDwith no base tables). This does not affect an in-place apply to the existing prod DB, but it does affect disaster-recovery / new-environment provisioning β track separately.
1. Migrations (apply together, in this order)
All under supabase/migrations/, all 2026-06-20:
| # | File | What it does |
|---|---|---|
| 182 | β¦_182_multiplier_pricing.sql | pricing_config / pricing_products / pricing_plans / pricing_multipliers + product_price_cents(). Additive. |
| 183 | β¦_183_plan_aware_consume.sql | Repoints consume_usage onto product_price_cents Γ quantity (cents), allowance-first. |
| 184 | β¦_184_plan_allowance.sql | apply_subscription_allowance / clear_subscription_allowance. |
| 185 | β¦_185_cents_denomination_personal.sql | Γ100 backfill of personal balances + ledger; handle_new_user welcome grant β 1000Β’. Guard: pricing_config.personal_cents_migrated. |
| 186 | β¦_186_legacy_deduct_to_cents.sql | deduct_credits / deduct_credits_idempotent scale creditsβcents (Γ100) internally; creates refund_credits (was missing in prod β refunds silently no-opped). |
| 187 | β¦_187_cents_denomination_teams.sql | Γ100 backfill of team pools/quotas/ledger; deduct_team_credits priced via the matrix 'team' column (drops old 5-arg signature); deduct_team_credits_idempotent Γ100. Guard: pricing_config.teams_cents_migrated. |
Apply mechanism: the Supabase migration runner (supabase db push against the
linked project) or, equivalently, apply_migration per file via the Supabase
tooling β in numeric order. 185 and 187 are guarded against double-application;
re-running is safe. Do not apply 186/187 before 185 (they assume cents balances).
2. Code stack (merge in order, then deploy)
Linear PR stack β merge bottom-up:
- #1246
feature/multiplier-pricingβ engine (182β184), Phases AβG1. - #1247
feature/pricing-g2aβ personal cents + legacy-RPC fixes (185, 186). - #1251
feature/pricing-g2bβ teams cents (187). - #1253
feature/pricing-g3β org-chart grants β cents. - #1255
feature/pricing-displayβ cents β dollars UI.
Deploy targets (all must ship the merged code)
- AWS Lambda = prod primary API (
index-aws.ts). Build + deploy the Lambda. This is the path live customers hit atapi(-pdf).theaccessible.org. - Node server on
10.1.1.4(index.ts):npm run rebuildfrom the Mac (zero-downtime restart). Node does not auto-deploy on push and must be rebuilt for non-additive migrations to be matched by code. - Cloudflare apps β
apps/web(PDF app),apps/org-chart,apps/home, and any other UI that reads balances. Deploy each (npm run deploy/ Wrangler). - The standalone
workers/org-chart-apiworker is superseded (no traffic) and is not part of this cutover β see follow-ups. Do not rely on it.
3. Cutover sequence (maintenance window)
Oldβnew are not wire-compatible, so minimize the gap:
- Announce a short maintenance window; optionally pause the SQS convert workers / put the apps in read-only if you want zero in-flight charges.
- Merge the stack (#1246 β #1255) into
main. - Deploy the code to all targets (Lambda + Node
rebuild+ Cloudflare apps). Get them all on the new code first β new code tolerates a pre-migration read better than old code tolerates a post-migration read, but the window between deploy and apply should still be minutes, not hours. - Apply migrations 182β187 in order.
- Smoke-test (section 4).
- Resume workers / lift read-only; close the window.
Rationale: the dangerous state is old code + migrated data (old code reads Γ100 balances as credits β users appear to have 100Γ funds; team charges hit PGRST203). Deploying code before applying migrations avoids that specific failure; the reverse (migrate then deploy) creates it. So: deploy, then apply.
4. Smoke tests (immediately after apply)
- Personal balance display β
/settingsshows a dollar balance that equals the pre-cutover credit count Γ $1 (e.g. an 80-credit user now reads$80.00). - Convert charge β run a 1-page PDF convert; confirm one
consume_usagecharge ofproduct_price_cents('pdf_convert', plan)cents and the balance drops by that. - Gate β a near-empty account hits the InsufficientCredits dialog showing dollars.
- Estimate β the pre-conversion estimate panel shows
$X.XX. - Org-chart β a chart extraction charges $1.00 (deduct_credits Γ100); the
org-chart
/settingsbalance reads dollars. - Stripe (test mode if possible) β a top-up grants
session.amount_totalcents; the ledger row is in cents. - Refund β
refund_creditsnow exists; a file refund credits the exact cents charged (previously a silent no-op). - Admin β grant/refund in the admin UI moves the balance by the dollar amount entered.
5. Rollback plan
There is no down-migration (the Γ100 backfill is lossy to reverse precisely once new charges land). Options, in order of preference:
- Fix-forward. Given the tiny dataset, most issues are correctable with a targeted SQL fix. Prefer this.
- Point-in-time restore. Supabase daily backups + PITR β restore the DB to just before the apply, and roll the code back to the pre-merge commit. Only viable if caught quickly and before meaningful new activity.
- Code-only rollback is NOT safe once migrations are applied β old code on migrated (cents) data is the exact broken state this runbook avoids. If you roll back code, you must also restore the DB.
Capture a manual snapshot right before the apply:
supabase db dump > backup-pre-cents-$(date +%Y%m%d).sql (store in R2).
6. Post-cutover follow-ups (tracked, not blockers)
- β
Delete the dead
workers/org-chart-apiworker and its CI wiring β done in PR #1258. - β
Remove the vestigial
creditsPerPageadmin system setting β done in PR #1259 (no reader remained after the estimate endpoint moved toproduct_price_cents). - Fix the fresh-DB migration replay (the
MIGRATIONS_FAILEDbranch finding) so disaster-recovery / new environments can rebuild fromsupabase/migrations. Root cause (diagnosed 2026-06-21): the very first migration20250213_001_credits_system.sqldeclaresREFERENCES auth.users(id)(onprofiles,credit_balances,credit_transactions,customer_pricing) plus a trigger onauth.users. On a brand-new Supabase branch theauthschema /auth.userstable isnβt guaranteed present when migrations replay, so 001 fails and nothing downstream runs. (A normal Supabase project hasauth.usersat creation β which is why prod was fine; this only bites a fresh branch/replay.) Same dependency in..._002_webhook_events.sqland..._004_multi_tenancy.sql. Fix options: (a) ensure Supabase Auth is initialized before the migration runner starts; (b) split 001 so tables are created first and theauth.usersFKs/trigger are added in a later migration; or (c) guard the auth-dependent statements behind an existence check. Low urgency (does not affect in-place prod), but blocks clean DR rebuilds. Do not edit 001 speculatively without a fresh-DB test confirming the fix. - Marketing reconcile β v1
CONVERTER_TIERScards (advertise $0.30β$0.75/page) vs v2PlanPricingTable(the real charge is $0.50β$1.00/page) on the homepage. The hardcoded v1 numbers now understate the actual price β needs a product decision on the pricing presentation (keep tier cards updated to v2, or replace with plan cards) before changing live advertised pricing. Independent of this cutover.