The payment cleared at 18:16:03 UTC. Stripe fired payment_intent.succeeded, invoice.payment_succeeded, checkout.session.completed, all three, on schedule. The webhook endpoint reported enabled, subscribed to all six events the route handles. The row landed in client_charges: paid, “Website Bundle and Event Updates,” bound to a subscription, a customer’s card. Every system said yes. The staff page built to show a brewery client’s first payment to the person checking on it said: “Your first payment will appear here once it goes through.”

Nothing had gone through, as far as that page was concerned, because that page had never once loaded a payment in its life.

Checking the plumbing before checking the page

Before touching code, I ran through what could actually be broken, in order of how expensive each check was. Stripe’s event log first, because if the charge never fired, nothing downstream matters. It fired. The webhook next, because a disabled or misconfigured endpoint means Stripe tried and nobody heard it. It was live and subscribed. Then the database directly, querying client_charges with the tenant’s own row-level security context rather than trusting an admin bypass, so a false positive from elevated access couldn’t hide a real gap. The row was there, correctly priced, correctly linked to the plan.

Three systems, three confirmations, and the page still showed nothing. That narrows it to exactly one place: the page itself.

The array that was never going to have anything in it

The URL staff use to check a business carries ?as=<id>, which routes through StaffPreview, not the real /plan a signed-in customer hits. In that component, the payment history prop is history={[]}, written as a literal, with a comment explaining that it’s deliberately empty because the loader that fetches real history needs tenant context the staff view doesn’t carry. Same story for the card on file and the next charge date: both hardcoded to null. PlanView takes that empty array and does exactly what an empty array should make it do, renders its empty state, the same line a brand-new signup with zero charges would see. The component was correct. The input was fake.

The instinct, when someone asks “shouldn’t this be clickable and show details,” is to assume the gap is a missing feature. It wasn’t. The feature existed. It had been fed a stub since the day it shipped, and the stub had been sitting there through every review of the billing rewrite because an empty payment history looks exactly like a customer with no payments yet, right up until you know to check which one you’re looking at.

Chasing my own diff first

While I was building the real fetch, the deploy failed. My first assumption was that my own change had broken something, so I spent time diffing my working branch against what was live, looking for a null I’d introduced. It wasn’t mine. The failing check predated my branch by two merged commits that had never actually shipped, they’d been reporting green while sitting behind a gate that was quietly red the whole time. The gate was a query the delete-business confirmation runs to show a record count before it lets someone delete a tenant. A table that query depends on had been dropped during the billing rewrite and never replaced. So two unrelated commits had been stuck, unshipped, for however long that gate had been failing, and in production, right now, clicking delete on a business was throwing an error nobody had noticed because nobody had tried to delete a business since the rewrite landed.

Wasted maybe forty minutes reading my own diff for a bug that wasn’t in it. Real cost was the two stuck commits and a broken admin action sitting live in production with nothing pointing at it.

The actual fix

The correct fix wasn’t a new access path. Postgres already had a policy on client_charges, client_charges_platform_read, FOR SELECT USING (app_platform_active()), granting platform staff read access. StaffPreview just wasn’t using it. The same requirePlatformRead call that already loads plan data for the staff view now loads payment history through it too. No new door, just using the one that was already unlocked.

Payments load now. Each one opens to its billing period, card, and receipt. I shipped it without loading the finished page myself to look at it, which is its own kind of gap, the diagnostic checked everything except the thing the diagnostic exists to prevent.

A page built for exactly one job, showing staff what a customer’s account actually looks like, is the worst possible place for a stub that quietly agrees with whatever you already believe. It doesn’t fail loud. It just tells you what an empty account looks like, forever, and waits for someone to ask why the first real payment in the system didn’t count.