← Writing

Repricing a live SaaS: the data-normalization checklist nobody gives you

Changing your pricing model is easy on a whiteboard. The real work is normalizing every existing customer, both payment modes, every tier reference in code, and the leftover limits that bite weeks later.

On this page

Every pricing-change announcement you’ve ever read skips the part that actually takes the time. Writing the new pricing page is an afternoon. The weeks go into what happens to everyone who signed up under the old model.

I went through this on an AI-agent platform I work on as fractional CTO: we replaced a set of legacy subscription tiers with a new plan structure, in a different currency, with different included usage. Roughly 140 production projects existed under the old model when we flipped the switch. Here is the checklist I wish someone had handed me, in the order the problems actually surfaced.

1. Decide what every existing customer becomes

A new pricing model doesn’t just add plans — it orphans the old ones. Every live project needs an explicit destination: grandfathered, migrated to an equivalent new plan, or dropped to pay-as-you-go. We chose to normalize everyone onto pay-as-you-go and let them opt into the new plans deliberately.

Whatever you choose, make it a migration with a script and a count, not a policy statement. “Existing customers move to X” is not done until you can query the database and see zero projects still referencing a legacy tier.

2. Cancel old subscriptions in both payment modes

Here’s the one that surprised me: your payment provider has a test mode and a live mode, and if the product has been through early development phases, real customer records can point at test-mode subscriptions. We found accounts whose subscription IDs existed only in Stripe’s test environment — customers who had, in effect, never been billed live at all.

So the cancellation sweep has to run twice, against both modes, and the results have to be reconciled against your own database rather than trusted blind. A subscription your database believes in but Stripe can’t find is not an error to skip past — it’s a record telling you your billing history isn’t what you thought.

3. Clear stale provider references en masse — in parallel

After cancellation you’re left with stale customer IDs and subscription IDs scattered across your records. Clearing them one account at a time through sequential API calls will take ages; we only got through it at a reasonable speed after parallelizing the sweep. Budget for rate limits, make the script idempotent so it can be re-run safely, and log every mutation — this is billing data, and you want an audit trail of exactly what was cleared and when.

4. Re-point every tier reference in code

The old tier names don’t just live in the plans table. They live in entitlement checks, in analytics events, in email templates, in support macros, in admin dashboards. We had to chase every reference to the legacy tiers through the codebase and re-point or delete it.

Grep for the tier names, then grep for the tier IDs, then grep for the environment variables and constants that held them. All three searches find different call sites.

5. Remove the feature gates, not just the plans

Frontend feature-gating tied to dead tiers is the sneakiest category. A component that renders “Upgrade to Pro to unlock this” is now advertising a plan that doesn’t exist. Worse, a gate that checks tier === "advanced" now silently locks a feature away from everyone, because nobody has that tier anymore.

Walk the UI as a normalized customer after the migration. Every gate either maps cleanly onto the new model or it comes out.

6. Hunt the long tail of per-tier limits

This is the checklist item I earned the hard way. Weeks after the migration looked complete, purchases of phone numbers started failing for some customers. The cause: a per-tier purchase limit left over from the old model, still enforced in a code path nobody had associated with “pricing.”

Limits are pricing. Quotas, caps, rate limits, included-usage counters — anything that ever varied by tier is part of the pricing model and has to be inventoried with it. If you only migrate the things called “plans,” the things called “limits” will page you later.

7. Keep watching after you’re “done”

The honest ending: follow-on bugs kept surfacing for weeks. Each one was small — a stale reference here, a leftover gate there — but each one was a customer-visible failure. Treat the migration as a monitoring project for at least a month after the cutover: watch for errors mentioning old tier names, watch purchase and upgrade flows specifically, and keep the migration scripts around because you will run pieces of them again.

The checklist, compressed

  1. Every existing account gets an explicit, scripted, countable destination.
  2. Cancellation sweeps run against both test and live payment modes, reconciled against your own DB.
  3. Stale provider IDs cleared by an idempotent, parallelized, logged script.
  4. Tier references chased through code by name, by ID, and by constant.
  5. Every frontend feature gate re-mapped or removed — verified by walking the UI as a migrated user.
  6. Every per-tier limit, quota, and cap inventoried as part of pricing.
  7. A month of post-migration monitoring, with the scripts kept warm.

None of this is glamorous. All of it is the difference between “we changed our pricing” and “we changed our pricing and then spent six weeks apologizing to customers.”

← All writing Book a call →
Book a call → WhatsApp