What Launching Billing Taught Me About Getting Paid
ResuPals went from a free client-side tool to a product with accounts, plans, and Stripe. Every hard lesson was about the gap between a payment starting and the money actually arriving.
ResuPals launched in January as a resume builder that ran entirely in your browser. No accounts, no server, everything in localStorage. That version is still there and still the front door.
In August it became a product. Accounts, cloud sync, a master career profile, AI job tailoring on credits, four paid tiers, and Stripe. Here is what that actually cost me to learn.
Say the true thing about privacy
The first change was to the copy, not the code.
The old pitch was "your data never leaves your browser." That was true and it was a real differentiator. The moment accounts shipped, it stopped being true for anybody with an account, and continuing to say it would have been a lie by the most generous reading.
So the product now says two things. Guest mode is local, genuinely, and remains a complete way to use the app without ever creating anything. Account data lives in a database I run, is not mined and not sold, and is exportable at any time.
That is a weaker sentence than the one I had. It is also true, and a privacy claim you have quietly outgrown is worse than a modest one you can defend.
Wait for the money
The single most important thing I learned, stated as a rule: grant the plan when the money confirms, not when checkout begins.
The naive flow starts a checkout, gets a success redirect, and upgrades the account. It works in testing, because in testing payments succeed. In production a card declines after the redirect, or the session expires, or the customer closes the tab at the wrong moment, and you have a free user sitting on a paid plan.
The corollary is that checkout events race. A customer can pay and land back on your site before the webhook telling you they paid has arrived. If the page trusts whichever signal shows up first, a paying customer sees the free plan and reasonably concludes they have been charged for nothing. Both paths have to reconcile against the same state rather than each claiming authority.
I found that one the way you find these things, which is that it happened to a real person on launch day.
Charge once
Related and separate: a retried operation must not double charge.
AI tailoring runs on credits. A tailoring run that fails partway and gets retried was, briefly, capable of debiting twice. The user sees one result and pays for two. Nobody notices except the person it happened to, and they lose trust in the number on the screen, which is the whole basis of a credit system.
Any operation that spends something needs to be idempotent on retry. Not "unlikely to be retried." Idempotent.
The law has opinions
I had assumed consumer subscription rules were a large-company problem. They are not, and they are not vague.
There is now a published refund policy, explicit withdrawal consent collected at the right moment instead of buried, and a renewal warning that goes out before annual plans charge again. The renewal reminder goes to annual subscribers specifically, because that is who the requirement is about, and sending it to monthly subscribers would be noise that trains people to ignore it.
None of this was hard. It was just work I had not budgeted for because I did not know it existed.
The migration that blocked every deploy
The infrastructure lesson, which is my favorite of the year.
The app moved to Cloudflare Workers with D1. At some point two migrations got applied straight to production with a direct execute command instead of through the tracked migration path. That records nothing in the tracking table. So the next tracked apply tried to create things that already existed, died, and blocked every deploy to main.
The fix was not to be more careful next time. The fix was to delete the commands that made the careless path possible, so the only remaining way to apply a migration is the tracked, idempotent one that also handles a fresh database. The deploy script now prints the drift recovery steps on failure instead of a bare SQL error, and the rule is written down in three places.
A process that depends on remembering not to do the wrong thing is not a process. If a footgun exists, someone will eventually pull the trigger, and that someone is usually you at eleven at night.
Pricing something with a real marginal cost
Almost everything in the app is free to serve. Inference is not. That is the only part with a per-use cost that scales with enthusiasm.
So tailoring runs on credits rather than being folded into a flat rate. Flat-rate pricing over a variable cost means either you set the price for heavy users and overcharge everybody else, or you set it for average users and the heavy ones become a problem you resent. Credits make the cost visible and let people buy more if they want more, which is a more honest conversation than a fair use policy nobody reads.
The rest of the tiers are the ordinary things: templates, document slots, a public resume link, and the career profile that lets you fill in your history once and reuse it.
What I would tell myself in January
Build the billing flow assuming every step fails independently, because it does. Write down the compliance requirements before writing the checkout, not after. And make the dangerous operation impossible instead of discouraged.
The product is better than it was. The lessons all came from the same place, which is the gap between something starting and something being true.