Billing That Holds Up: Stripe Platform Billing
Photo: Words as Pictures · StockSnap (CC0 1.0), via Openverse
At some point, every platform has to answer the uncomfortable question of how it finances itself. For Xircuit, that was a sideline topic for a long time that I kept putting off because building the actual features seemed more important. This week the question could no longer be postponed: anyone seriously using the platform has to be able to pay for it, and in a way so uncomplicated that payment is never the reason someone bounces.
I deliberately took my time with it, because billing is one of those components you should get right exactly once. A half-baked billing system takes revenge later with duplicate bookings, lost cancellations, and support tickets nobody can resolve. This post describes how platform billing at Xircuit now works and which decisions are behind it.
Why Stripe and a Platform Model
The obvious decision was not to handle payments myself. Card data, compliance, recurring charges, tax rates, failed payments with retry logic: that's a product in its own right, and there's already one that does it well. Xircuit therefore bills subscriptions through Stripe, and Stripe holds everything that has to do with the actual flow of money.
More important than the choice of provider was the structure. Xircuit serves very different industries, from gyms to physical therapy practices to clubs, and I didn't want to maintain a separate billing model for each of them. Platform billing therefore covers all industries with the same foundation: one product catalog, one subscription logic, one set of states that every organization moves through. What differs between industries is the feature set, not the mechanics of billing.
This uniformity is more than a convenience for me as a developer. It means every improvement to billing immediately benefits all industries, and a bug I fix in one place is fixed everywhere. Billing copied per industry would have meant exactly the opposite: five slightly different variants, each carrying its own edge cases and its own silent bugs.
Webhooks as the Source of Truth
The trickiest part of any Stripe integration project is the webhooks. Stripe communicates asynchronously what happened: a subscription was created, a payment went through, a card was declined, a subscription was canceled. These events are the only reliable source for what state an account is really in. Anyone who instead trusts the redirect after checkout is building on sand.
That's why I put webhook processing into a dedicated service instead of letting it run somewhere inside the web frontend. That decouples receiving events from the user interface and fits the rest of the Aspire topology, where every responsibility gets its own clearly scoped service. The service receives the events, checks the signature, and processes them in the background.
Idempotence is crucial here. Stripe doesn't guarantee that an event arrives exactly once; it can deliver the same event multiple times, for instance after a timeout or a retry. Processing is therefore built so that an event arriving twice breaks nothing. Every event is recognized by its identity, and whatever has already been booked doesn't get booked a second time. That sounds unspectacular, but it's the difference between a billing system you can trust and one that silently double-charges on every network hiccup.
Photo: Words as Pictures · StockSnap (CC0 1.0), via Openverse
Self-Service Through the Stripe Customer Portal
As soon as money flows regularly, a second problem arises: people want to change their payment details, switch their plan, or cancel — and they want to do it without having to write an email. Building each of these interactions myself would be a lot of effort for something Stripe already solves cleanly.
I therefore connected the Stripe customer portal. Through a single entry point in Xircuit, an organization reaches a Stripe-hosted portal where it can update its payment methods, review past invoices, and manage its subscription itself. Whatever happens there comes back to us through the same webhooks, so the state in Xircuit is automatically kept in sync. Self-service here doesn't mean two systems drifting apart, but that Stripe provides the surface while Xircuit stays consistent via the events.
Coupling the Feature Set to Subscription Status
A subscription only makes sense if the paid state actually makes a difference. At Xircuit, the feature set is therefore coupled to subscription status. This gating doesn't build on some separate special path but on the feature flag infrastructure that's already in the system and that's used to switch individual platform capabilities on and off.
The advantage of this reuse is that gating and feature control speak the same language. Subscription status decides which flags apply to an organization, and the application asks at exactly the same places it already asked about capabilities before. There's no second, parallel logic that would need to be maintained and tested separately. When status changes due to a Stripe event, the available feature set shifts along with it, without anyone having to intervene manually anywhere.
Deliberately Still in Test Mode
One point that matters to me because it's easy to overlook: in production, Stripe's TEST keys are currently running, deliberately. The entire chain stands, from checkout through the webhooks to the customer portal, but no real money is being moved yet. That's not an oversight, it's a decision.
I want to first observe billing under real conditions before it actually charges anyone. With test keys, I can create subscriptions, simulate payments, run through declined cards, and check webhook idempotence under load, without a bug costing anyone real money. Switching to live keys is therefore deliberately a separate, later decision. Only once I'm sure every state transition sits cleanly will the switch happen. Until then, TEST mode isn't a stopgap, it's a safety line.
What I'm Taking Away From This Week
In hindsight, what took effort this week wasn't the happy path where someone signs up for a subscription and everything works. What took effort were the edges: the event delivered twice, the declined card, the cancellation triggered in the portal that has to be reflected in Xircuit. But it's exactly those edges that decide whether a billing system holds up or only lasts until it first gets tested for real.
That billing stands on the same foundation across industries, that webhooks run idempotently in a dedicated service, that self-service is delegated to Stripe, and that gating builds on the existing feature flag infrastructure, together add up to a system I can switch live with a clear conscience once the timing is right.
What's Next?
Next week: how organizations bill their own members — through Stripe Connect.
Comments
No comments yet. Be the first to share your thoughts!
Leave a Comment