We Built the Same Billing Layer Five Times. Here Is What We Learned.
Five products, five Stripe integrations, one slowly dawning realization. A post-mortem on duplication — and the rule we now use to decide what belongs on the platform.
If you want to know whether a company has a platform or a story about a platform, look at how many times it has implemented billing.
We had five.
How it happened
It was not carelessness. Each product had a real reason to be different. CapturedIt needed personal and team scopes with structurally separate keys. Premodoro needed Stripe on the web and Apple in-app purchase on Safari. HuniDu needed per-pillar subscriptions a household could turn on and off. Bread & Butter needed seats, roles, and an invoice that a finance team could read. nMyCity needed almost nothing — until it needed a tenant.
Five legitimate shapes. Five implementations. Five sets of webhook handlers that each fixed the same race condition on a different Tuesday.
The moment it became obvious
The tell was not the code. It was the dashboards. We had five places to look when someone said "I paid and I still can't see the feature." Each one answered a slightly different version of the question.
That is when a shared layer stops being an engineering nicety and becomes an operations requirement. You cannot run support across a portfolio when every product has a private definition of "entitled."
The rule we use now
Something goes on Rumble Rails when all three are true:
- At least two products need it. One product is a feature. Two is a pattern. Five is a tax.
- Getting it wrong costs money or trust — billing, identity, telemetry, provider failover. Nobody churns because your empty state is ugly. People churn because they were charged twice.
- It has nothing to do with what the product is for. Nobody chooses a household app for its Stripe webhook handling. If a capability is not part of why a customer picks the product, it belongs on the rails.
Everything else stays in the product, where it can be opinionated.
What a shared billing line looks like
On the Rail Map, billing and entitlements is one line. In practice it is a small set of promises every product can rely on:
- One definition of a plan, a seat, and a limit — and the UI, the API, and the invoice all read from it.
- Webhooks handled once, idempotently, with the race conditions already paid for.
- Metering that works the same whether the unit is a save, a widget, or a seat.
- A single place to answer "what is this account entitled to right now, and why."
Premodoro runs fully on it today. The others are being connected one at a time, and the map on the platform page tracks which. We would rather show a dashed line than claim a solid one.
The uncomfortable part
Building the fifth billing integration felt productive. It shipped a product. Pulling the shared layer out felt like a detour. It shipped nothing a customer could see.
It was the most leveraged work we did this year.