Payment Couldn’t Be Easier (Famous Last Words)
In theory, adding payments should be the easy part. Stripe has spent years perfecting the art of separating people from their money with minimal friction. All I had to do was wire it up.
The flow looks beautifully simple on paper:
- The customer attempts to sign in. If their trial and grace period have expired, the server politely replies, “Payment required.”
- The authentication modal stays open and reveals a shiny [Pay] button.
- The user clicks [Pay], and the JavaScript fires off a
POST /api/billing/checkoutwith the session headers. - The server validates the session and returns a hosted Stripe Checkout URL, complete with
client_reference_id=<customerId>. - The browser opens that URL in a new tab.
- The user parts with their money in Stripe’s UI.
- Stripe completes the payment.
- Stripe calls my webhook at
/api/billing/stripe/webhook. - The webhook verifies the call using
Stripe:WebhookSecret. - The server marks the customer as paid by setting
PaidAtUtc,PaidUntilUtc, andIsPaid=true. - The server returns
isPaid=true(or a futurePaidUntilUtc), and the app proceeds.
That’s the theory. We’ll find out soon enough whether reality agrees.
Running a Pilot Without Losing My Mind
Before unleashing this on the world, I need a pilot phase — invite‑only, controlled, and ideally free of chaos.
The plan:
- Disable the [Register] button.
- Wrap the registration API in
#if IN_PILOT_PHASEso nobody can bypass the UI and call it directly. - Return a 403 for anyone attempting to register without an invite.
- Send invited users a username and password manually.
- Give them unlimited grace if the pilot runs longer than 30 days, with the understanding that their feedback earns them a future paid‑up period.
It’s not glamorous, but it works.
