The Pricing Capabilities You'll Need Before You Think You Need Them
A staged framework for knowing which pricing capabilities matter at each phase of a SaaS, and what breaks the day you need one you didn't build.
The stages, roughly
Three stages, not because reality is that clean but because the pressure changes shape at each one.
Pre-revenue: You have an idea of pricing, maybe a pricing page, maybe zero paying customers.
Early paying customers: Somewhere between your first customer and a few hundred. This is the stage most solo founders and small teams live in for a long time, sometimes years.
Scale: You have enough customers and enough revenue that any pricing change touches real cohorts, real support volume, real churn risk.
Most of the harm I've seen (and caused) comes from building scale-stage infrastructure at the pre-revenue stage, or, more often, building nothing at all until you're deep into the early-customers stage and a single email forces your hand.
Pre-revenue: build almost nothing, decide the shape
At pre-revenue, you genuinely don't need A/B price testing. You don't have the traffic for it to reach significance, and running an experiment on twelve visitors is theater, not data. You don't need grandfathering because you have nobody to grandfather. Multi-currency is very likely a waste of a sprint.
What you do need, and what's cheap to get right now and expensive to retrofit later, is the shape of your data model. Specifically: does your system treat a product and its price as the same row, or as two things that happen to be related right now. This single decision determines almost everything downstream. If price is a column on the product table, every future pricing change becomes a migration on your core entity. If price is its own table with a foreign key back to product, a pricing change becomes an insert.
I split these at Refract into a products table and a separate prices table, with a partial unique index enforcing exactly one default price per product at a time. It sounds like over-engineering for a pre-revenue product with three plans. It isn't, because the alternative isn't simpler, it's the same complexity, deferred and compounded, arriving later as an incident instead of a design decision. Stripe itself models prices as objects distinct from products for the same reason, and it's worth noticing that even Stripe won't let you mutate a price once it exists. You archive it and create a new one. That constraint from the vendor is a hint about what the right internal model looks like, whether or not you're using Stripe.
The other pre-revenue decision that's cheap now: don't couple your feature access checks to plan names as strings. if (user.plan === 'pro') { ... } is the same mistake as the hardcoded price ID from the intro, just wearing a different shirt. A scope or capability system, however lightweight, costs you an afternoon here and saves you a rewrite later, because the plan names will change and the scopes underneath them mostly won't.
Early paying customers: this is where it actually breaks
This is the stage where the theory stops being theory. You have real people, on real plans, and the first time you touch pricing after they've signed up is the first time you find out what you actually built.
Grandfathering. Your first fifty to five hundred customers are going to be on whatever pricing you had when they signed up, and at some point you're going to want to change that pricing without punishing them for having found you early. The failure mode isn't "we don't have a beautiful grandfathering system." Nobody expects that at this stage. The failure mode is that grandfathering wasn't a concept in your system at all, so when it's needed, someone writes a one-off script, or a conditional in the billing webhook handler, or a spreadsheet of customer IDs that "should stay on old pricing" that lives in someone's head and nowhere else. I've seen support tickets spike two hundred to five hundred percent in the weeks after a poorly handled feature removal, and almost all of that volume traces back to the same root cause: there was no durable record of who was entitled to what, so every exception had to be handled by a human, individually, under time pressure.
The fix isn't complicated in principle. A plan needs a stable identity that survives price changes underneath it. If your products and prices are already split, this is almost free, you just don't force everyone onto the new default price when you create it, you let existing subscriptions keep referencing their old price object and only apply the new default going forward.
Feature deprecation. You will kill a feature. Maybe an integration nobody uses, maybe a legacy tier that's costing you support time for revenue that doesn't justify it. The two ways this goes badly are both about surprise. Either the customer finds out because something silently stopped working, or they find out because you emailed them with thirty days' notice and no path forward. Neither is really a pricing problem, they're a communication and sequencing problem that pricing infrastructure happens to make easier or harder. If a "product" in your system can be retired independently of the customers currently subscribed to it, you can run the deprecation clock without touching anyone's active billing. If a feature is hardcoded into a plan check, you can't retire anything without a migration that touches every customer at once, on your timeline, not theirs.
Plan migrations. Somebody upgrades, somebody downgrades, somebody moves from monthly to annual. Proration exists so this doesn't feel like a scam. It's worth saying plainly that this is the part of the whole staging framework I'd protect first if I had to pick one, because a botched proration is the kind of bug that turns into a refund request and a one-star review on the same day. Not catastrophic individually. Corrosive in aggregate, and it erodes trust in exactly the customers you can least afford to lose this early.
None of this needs to be built as a platform at this stage. It needs to be built as a small number of correct primitives: prices that don't mutate, products that can be retired independently of active subscriptions, and a way to know, for any customer, exactly which price object they're actually on right now, not which plan name shows up in your marketing copy.
Scale: now the experiments start paying for themselves
At scale, the economics flip. A/B testing pricing stops being theater because you finally have enough volume for a test to reach statistical significance in a reasonable window, and the data on this is fairly consistent: companies that test pricing regularly grow meaningfully faster than those that don't, and a single well-run test can move revenue by a wide margin. The catch, and this is the part checklist articles tend to skip, is that pricing tests have lower win rates than UI tests, something in the range of fifteen percent versus much higher for simple interface changes. You will run several tests that do nothing or that make things worse before you find one that works. That's expensive in a way that's easy to underestimate if you've only ever run UI experiments before.
Multi-currency shows up here too, usually forced by a specific deal rather than a strategic decision, a large customer in the EU or the UK who wants to pay in their own currency and whose finance team won't budge. If your prices table already treats "price" as a first-class object rather than a derived field, adding a currency variant of an existing price is additive. If price has been a number bolted onto the product row this whole time, it's a schema change under deadline pressure from a sales team that wants the deal closed this week, and that's a bad position to negotiate a data model from.
Feature-level packaging, the ability to compose a plan from independent capability flags rather than choosing from three fixed tiers, matters most here because at scale you start discovering that your tier boundaries don't match how customers actually want to buy. Someone wants the enterprise support tier but none of the enterprise features. Someone wants one specific integration and is otherwise happy on the starter plan. This is the point where a scopes-based access model, if you built it early, starts paying rent instead of just sitting there as tidy architecture. I'll admit I didn't fully believe this one until I watched it happen, I'd assumed the scopes system was mostly about clean code and it turned out to be about sales flexibility instead.
A short note on processors
There's a version of this whole argument that applies to your payment processor, not just your pricing model, and it's worth flagging even though it's not the main event here. Refract runs on Stripe today, and Stripe is genuinely good, but the same discipline that keeps your mailer or your deployment target swappable behind an adapter should apply to billing too. Not because you expect to leave Stripe. Because "the contract you wrote against a specific vendor's API" and "the business logic of what a price, a product, and a subscription mean to your company" are two different things, and conflating them is how a future processor migration becomes a rewrite of your entire billing domain instead of a new adapter implementation. This is one more staged capability, not the headline one, LemonSqueezy and Paddle adapters are a later-stage concern for us, not a today concern. But the seam should exist before you need it, same as everything else in this framework.
Where this leaves you
None of these capabilities are hard, individually, in isolation. What's hard is that they all compete for attention against features that customers are asking for by name, and pricing infrastructure has no customer asking for it by name until the day it's missing and something breaks in front of them. I don't have a clean rule for how much of this to build before you have revenue to justify it. I lean toward: get the data model right early because it's nearly free, and build the actual grandfathering or migration tooling only once you have a customer forcing the question, because building it in the abstract tends to guess wrong about what you'll actually need.
There's a newer wrinkle to this too. Ask an AI model to add a pricing feature and it will happily write you a working column on the product table, because it has no reason to guess that you'll need to grandfather someone in eighteen months. It optimizes for the ticket in front of it, not the migration you haven't hit yet. That's not a knock on the tools, it's just what they are, pattern-matching against the request as written. The judgment about what the request is actually going to cost later still has to come from somewhere, and right now that somewhere is still a person who's been burned by the hardcoded version before.
FAQ
- At what stage should I actually start building grandfathering logic, not just designing for it?
Roughly, the moment you have paying customers and you're considering any change to pricing or plan structure, even a small one. You don't need a full system before that. You do need your data model to already support it, which is why the products/prices split belongs earlier than the grandfathering logic itself.
- Isn't A/B testing pricing too risky for a small SaaS anyway?
The perceived risk is usually higher than the actual risk, which is part of why only a small share of SaaS companies test pricing regularly despite the growth data favoring those that do. The real constraint at small scale isn't risk, it's volume, you often don't have enough traffic for a test to mean anything statistically.
- Do I need multi-currency support before I have an international customer asking for it?
No. This is one of the clearer cases where building ahead of demand is wasted work. What you want ahead of demand is a price model where adding a currency variant is additive rather than a schema change under deadline pressure.
- Is this just an argument for using Stripe's data model as my own?
Not exactly. It's an argument that whatever your internal model is, it shouldn't be more mutable than the vendor you're likely to build on assumes it should be. Stripe not allowing price mutation is a signal worth listening to independent of whether you use Stripe.
- How does this connect to feature flags and access control?
A scopes or capability-based access model is what lets feature deprecation and feature-level packaging happen without touching plan names directly. It's the same underlying idea as the pricing split: separate the stable concept (what a customer can do) from the thing that changes over time (what plan or price that's currently attached to).