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.

Engagement
Dedicated team
Published

Outcomes

What changed, and how we know

  1. 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
  2. 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
  3. 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.

Similar problem?

If you are working on something like this in saas & marketplaces, tell us what you are measuring today and we will tell you whether we can move it.