Skip to content

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_credits signature, 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_balances rows (max balance 8,243 β†’ Γ—100 = 824,300, far under the int4 ceiling of 2,147,483,647), 4,201 credit_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 committed supabase/tests/*.test.sql suites cover each.
  • Known caveat: the full repo migration history does not replay cleanly on a fresh Supabase branch (branch came up MIGRATIONS_FAILED with 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:

#FileWhat it does
182…_182_multiplier_pricing.sqlpricing_config / pricing_products / pricing_plans / pricing_multipliers + product_price_cents(). Additive.
183…_183_plan_aware_consume.sqlRepoints consume_usage onto product_price_cents Γ— quantity (cents), allowance-first.
184…_184_plan_allowance.sqlapply_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.sqldeduct_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:

  1. #1246 feature/multiplier-pricing β€” engine (182–184), Phases A–G1.
  2. #1247 feature/pricing-g2a β€” personal cents + legacy-RPC fixes (185, 186).
  3. #1251 feature/pricing-g2b β€” teams cents (187).
  4. #1253 feature/pricing-g3 β€” org-chart grants β†’ cents.
  5. #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 at api(-pdf).theaccessible.org.
  • Node server on 10.1.1.4 (index.ts): npm run rebuild from 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-api worker 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:

  1. Announce a short maintenance window; optionally pause the SQS convert workers / put the apps in read-only if you want zero in-flight charges.
  2. Merge the stack (#1246 β†’ #1255) into main.
  3. 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.
  4. Apply migrations 182–187 in order.
  5. Smoke-test (section 4).
  6. 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 β€” /settings shows 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_usage charge of product_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 /settings balance reads dollars.
  • Stripe (test mode if possible) β€” a top-up grants session.amount_total cents; the ledger row is in cents.
  • Refund β€” refund_credits now 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:

  1. Fix-forward. Given the tiny dataset, most issues are correctable with a targeted SQL fix. Prefer this.
  2. 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.
  3. 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-api worker and its CI wiring β€” done in PR #1258.
  • βœ… Remove the vestigial creditsPerPage admin system setting β€” done in PR #1259 (no reader remained after the estimate endpoint moved to product_price_cents).
  • Fix the fresh-DB migration replay (the MIGRATIONS_FAILED branch finding) so disaster-recovery / new environments can rebuild from supabase/migrations. Root cause (diagnosed 2026-06-21): the very first migration 20250213_001_credits_system.sql declares REFERENCES auth.users(id) (on profiles, credit_balances, credit_transactions, customer_pricing) plus a trigger on auth.users. On a brand-new Supabase branch the auth schema / auth.users table isn’t guaranteed present when migrations replay, so 001 fails and nothing downstream runs. (A normal Supabase project has auth.users at creation β€” which is why prod was fine; this only bites a fresh branch/replay.) Same dependency in ..._002_webhook_events.sql and ..._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 the auth.users FKs/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_TIERS cards (advertise $0.30–$0.75/page) vs v2 PlanPricingTable (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.