We just added recurring product subscriptions to StoreFlux (a headless commerce backend), and the design decision I want to write about is the one that felt obvious in hindsight but wasn't the first thing I reached for: we don't store a customer's card at all.
The instinct everyone has first
The "normal" way to build subscriptions is to tokenize the customer's card on the gateway (Stripe customer + payment method, say), save that token, and on each billing date fire an off-session charge against it. That's a completely reasonable design. It's also one most commerce platforms already have sitting around, because it's the obvious answer.
I didn't want to build that, for a fairly unglamorous reason: it's a second payment path. Checkout already has a payment path — tested, handles declines, handles 3D Secure challenges, handles webhooks, handles every gateway-specific weirdness we've already run into. A separate off-session billing path duplicates all of that, badly, because off-session charges can't show the customer a 3DS challenge if one gets triggered. You end up with checkout's payment logic and subscription-billing's payment logic silently diverging over time as each gets bugfixed independently.
What we did instead
A subscription doesn't store a payment method at all. On each billing cycle, a background job builds a normal cart from the subscription's line items and runs it through the exact same CheckoutCartCommand a human customer's browser calls when they check out themselves. Same validation, same stock checks, same tax calculation, same payment-provider dispatch. The subscription system's job is entirely "decide when the next cycle is due and what's in it" — it has no opinion about payments at all, because it never touches that code.
This means whatever payment method support checkout has, subscriptions automatically have too, with zero subscription-specific payment code. Add a new gateway to checkout, subscriptions get it for free. Fix a checkout payment bug, subscriptions get the fix for free. There's structurally no way for the two to drift apart, because there's only one.
The real tradeoff: no off-session silent renewal
The honest downside is that this doesn't work for "quietly keep charging in the background with zero customer involvement," the way a tokenized-card system does. Depending on exactly how checkout is wired for a given tenant's gateway config, a renewal might need an active customer session rather than a true fire-and-forget background charge — this is store-config-dependent since it rides on whatever payment flow checkout already uses. If a tenant specifically needs true zero-touch background billing with a stored card and off-session charging, this isn't that. It's a deliberate "reuse what's already correct" design instead of a maximally-automated one, and for v1 that tradeoff made sense: fewer payment code paths beats one extra convenience feature.
Why this mattered enough to write up
PCI scope is the other half of why this felt right. Not storing card data means not being in the business of protecting card data — no vault to encrypt, no extra attack surface, nothing to accidentally leak in a backup or a log line. "We don't have the data" is a stronger security property than "we encrypted the data well." Every stored-card system is one bug away from a very bad day; a system with no stored cards doesn't have that bug available to make.
Curious whether anyone else building subscription billing has made the same call, or deliberately gone the other way and found reuse wasn't worth losing silent off-session renewal. Feels like a real tradeoff either direction, not an obviously-correct one.
storeflux.store if you want to see where it landed in the actual product.
