Saltea
Rebuilding subscription billing and entitlements at Saltea
Every price change at Saltea needed an engineering release, and mid-cycle plan changes were priced three different ways. We moved pricing out of the application and rebuilt the billing surface on top of it.
Outcomes
What changed, and how we know
- 01
Time to publish a pricing change
Off the engineering critical path entirely
- Before
- An engineering release, scheduled against the product backlog
- After
- A catalogue change made by the commercial team, with no deployment
- Window
- From catalogue cutover onward
- Method
- Comparison of the client's own change records for pricing changes raised before and after cutover
- 02
Manual credit notes raised to correct mid-cycle pro-rata
Sustained across the full window
- Before
- A routine part of every month-end close
- After
- None raised for pro-rata in the measurement window
- Window
- Measured across the first four month-end closes after cutover
- Method
- Credit-note records from the client's finance system, filtered on the reason already recorded against each one
- 03
Implementations of the pro-rata rule in the codebase
Two removed
- Before
- Three, in checkout, renewal and invoicing
- After
- One, in the pricing engine
- Window
- At cutover
- Method
- Call sites counted in the shipped code at cutover, against the same count taken across the application at the start of the engagement
Challenge
Saltea sells a B2B workflow product on a mix of seat and usage pricing, billed monthly and annually, with a self-serve plan page and a sales-negotiated tier above it. Plans, entitlement grants and invoice logic had all grown inside the main application over six years.
Publishing a price or adding a plan meant a code change, a release and a regression pass, so commercial decisions moved at the speed of the engineering backlog. Mid-cycle upgrades were pro-rated by three separate implementations — one in checkout, one in the renewal job, one in invoicing — and finance reconciled the differences by hand at every month-end close.
Constraints
- No change to what existing customers were charged; every live subscription had to keep renewing on the terms it was sold under
- Revenue recognition ran off the existing invoice records, and the finance system's expectations of that schema could not move
- The product needed an entitlement answer on every request, so nothing could be added to the synchronous request path
- Sales-negotiated annual contracts sat outside the plan catalogue and had to keep working throughout
Approach
- Team
- Two senior full-stack engineers, one backend engineer, and a product designer at 50 percent once the finance console started; the tech lead was full time through the parallel billing run and 25 percent after it
- Engagement model
- Dedicated team
- Duration
- Eight months, including the parallel billing period
Two-week iterations with a parallel billing run at every boundary — the new engine priced every live subscription alongside the existing code, and each difference was explained before the next iteration started.
Architecture
We moved plan definitions, pricing rules and entitlement grants out of application code into an effective-dated catalogue with a single pricing engine over it, then rebuilt the customer-facing subscription pages and the internal finance console as clients of that engine, so a quote, a renewal and an invoice line are all the same calculation.
- TypeScript
- Node.js
- React
- PostgreSQL
- Redis
- Stripe
Key decisions
- An effective-dated catalogue rather than a mutable one
- Live subscriptions had to keep renewing on the terms they were sold under. Versioning prices by effective date let a new price be published without touching a single existing contract.
- One pricing engine behind checkout, renewal and invoicing
- The pro-rata disputes finance chased each month came from three implementations of one rule. Deleting two of them removed the class of problem rather than the instances.
- Entitlements resolved from the subscription, cached and version-stamped
- An entitlement check happens on every request, so a synchronous call into the billing service was not viable. A version stamp on the subscription let the cache invalidate on plan change without a blanket flush.
- Negotiated contracts modelled as explicit overrides on a catalogue plan
- Sales-negotiated terms were why the old code had accumulated special cases. Modelling them as overrides kept them inside the engine instead of beside it.
The parallel run did most of the work
For four months the new engine priced every live subscription alongside the existing code and wrote the result to a comparison table nobody billed from. Each difference was either a defect in the new engine or an inconsistency in the old one, and a meaningful share turned out to be the second — behaviour that had been quietly wrong for years and that some customers had been billed on.
Separating those two categories before cutover, rather than during it, is what let finance sign off. It also changed the conversation with the client's commercial team from whether the new pricing was correct to which of the old outcomes they wanted to keep.
What we would do differently
We built the customer-facing subscription pages before the internal finance console, because the customer-facing work was the more visible half of the engagement. That was the wrong order. Finance were the people reconciling the parallel run every fortnight, and for the first two months they did it against database queries we wrote for them by hand. The console should have shipped first.

