Flow
The gaps I'd close, in order
One step per gap found in this commit.
1Tests first
A paid team past the free cap, and a signed-in stranger asking for another team's portal.
2One seat-cap function
Fed by the plan and used by both the screens and the server, so the saved limit can go.
3A stable retry key
Built from the organization, so a retry gets the same customer back.
4A plan for outages
Either signup fails clearly, or the team is marked billing-pending and a job finishes it later.
5A membership check on each billing endpoint
Compare the requested team with the caller's team before calling the provider.
6Something that goes red
Count refresh presses and flag paying teams whose sessions show no plan.
Looking back through one of my own commits on the admin app, I found three different answers to "how many people can this organization have". Only one of them hears about it when a customer pays.
The copy I deleted
The app used to keep its own copy of billing data. A webhook took events from the payment provider and wrote them into tables. Health checks watched it. This commit deleted all of that, and I still think that was right. A copy that's only usually right causes bugs.
Now the plan comes from the user's session. The new code counts on the login provider putting the plan there as a list of tags. The app takes the highest plan on the list. No plan tag means free. A refresh button fetches a new session for someone who just upgraded.
Three answers about seats
Free organizations get a small seat cap. After the commit, three pieces of code answer the seat question:
- The session helper says free is capped and paid is unlimited.
- An interface helper ignores the plan and looks for a different, optional tag.
- The server reads a limit saved on the organization when it signed up.
Only the session learns about upgrades. The saved limit never changes, because the webhook that could have changed it is gone. The server check runs when someone joins. If the saved limit says full, it logs a warning and skips the new member. So a paid team that grows past the free cap could end up with people the login provider knows about and my app doesn't.
The cap should come from the plan, in one function, everywhere it's checked.
One function turns the plan into a seat cap, with the free plan getting the free limit and every other plan unlimited.
A random key and a required signup step
When the app creates a customer at the payment provider, it used to send a retry key based on the organization. A retry got the same customer back. Now the key is random each time. A lost reply plus a retry can make two customers for one organization.
Billing setup also used to be optional during signup. If it failed, signup went on. Now it's required, with no retry job and no "billing pending" state. A provider outage can now block or break signup, and with the random key a second attempt can leave an extra customer behind.
Checking who's asking
The new checkout, portal and seat count endpoints check that someone is signed in. They then trust whatever organization the request names. Nobody checks that the caller belongs to it. The billing portal is the worst case, since it shows invoices and has a cancel button. The session already knows the caller's organization, so comparing the two is cheap.
Losing the alarms
The old health checks could flag webhook problems. Now a broken sync just looks like a free organization. Nothing goes red. The refresh button is the only hint, since each press means someone sees the wrong plan, and those presses aren't counted anywhere. The commit adds no tests for the seat check or the new endpoints either.