Nobody starts a side project excited about sales tax. But if you charge money on the internet, the boring parts are load-bearing, and the decisions here took real evaluation. This chapter is the payments and licensing system: what a merchant of record is and why a solo developer almost certainly wants one, why we picked Paddle after evaluating three alternatives, and the Cloudflare Worker that turns "your email is the license" into an actual protocol.
What a merchant of record is, and why it matters
When you sell software directly, you are the seller, legally. That word carries more than it sounds like it does: collecting and remitting sales tax, VAT, and GST in every jurisdiction where you cross a threshold, handling chargebacks and fraud, honoring regional consumer law. The United States alone has thousands of tax jurisdictions; the EU wants VAT from the first euro. For a $29.99/year app sold worldwide by one person, doing that correctly is not a weekend of paperwork, it is an ongoing compliance operation that can dwarf the engineering.
A merchant of record (MoR) inverts the relationship: the platform is legally the one selling to your customer, and you supply the product to the platform. Their name is on the tax filings, the card statements, and the chargeback disputes. The going rate is around five percent plus a fixed fee per sale, and it is the best money this project spends. The alternative is not "keep the five percent", it is "become a small international tax firm."
Why Paddle, specifically
We use Paddle (5% + 50¢ at the time we signed up), and the decision was made twice: once at launch and once more when alternatives came knocking. We evaluated other platforms seriously, seriously enough to keep a designated fallback with a mechanical migration path in case Paddle ever misbehaves, and the notes from those evaluations reduce to one transferable lesson rather than a scoreboard.
The lesson: pricing pages advertise the take rate, but the real comparison is take rate plus payout fees to your actual bank plus whatever the platform's licensing model forces on your architecture, evaluated at your actual volume rather than aspirational volume. A headline-cheaper rate can cost more by the time the money lands, and a platform whose licensing layer dictates license keys would have vetoed the entire design in the next section. Run that arithmetic for your own shape of sale; the winner is not the same for everyone.
No license keys. Your email is the license
License keys are where support tickets are born: lost keys, keys pasted with a trailing space, keys shared on forums, keys living in an email account the customer abandoned. We refuse the whole category. Buying Text2Store creates no key; it makes your email address entitled. Signing in on a machine means typing your email, receiving a 6-digit code, and typing it in. That is the entire user-facing surface.
Under it there is an actual protocol, running on a Cloudflare Worker with a D1 (SQLite) database:
- Paddle webhooks mirror subscription state into D1. The Worker never asks Paddle at sign-in time in the normal path; it holds its own mirror, kept current by subscription lifecycle events. (It also self-heals: if a webhook was missed, sign-in can reconstruct a customer from the Paddle API.)
- The 6-digit code is treated like a password reset token: stored only as a SHA-256 hash, ten-minute expiry, five attempts, single use. And the request endpoint answers "ok" whether or not the email is a customer, so nobody can use it to test which addresses bought the app.
- A successful sign-in mints two things. An opaque, revocable device token (hashed server-side), and a compact entitlement signed with Ed25519 that the app verifies offline on every launch. The entitlement carries two dates: when the paid period ends (for display) and that date plus a 14-day grace window (for enforcement). A perpetual comp simply has neither.
- Renewals and refunds both arrive through the same quiet mechanism: a fail-open refresh on every app launch re-mints the entitlement. Renewal? The new dates just appear. Refund or cancellation? The mirror flips, and the device disables on its next start. Fail-open matters: a Worker outage can never lock out a paying customer, because yesterday's entitlement still verifies offline.
- Except where fail-open would cost us money in real time: the AI relay checks the subscription mirror on every request, so a refunded account loses paid inference immediately, not at next launch. Fail-open for the customer's access, fail-closed for our marginal costs. That asymmetry is deliberate.
Two design choices worth defending. Unlimited devices, enforced nowhere in code: one subscription covers every iPhone, iPad, and computer, because per-device pricing is the thing people resent about this category. And a lapsed subscriber can still sign in: they get their archive access state honestly, with a renew prompt, instead of a wall. The system remembers you were a customer.
The same Worker is the AI relay
One architectural convenience fell out for free: AI summaries need a server anyway (the client must never hold model API keys), and the licensing Worker already knows who you are and whether you are paid. So the /ai endpoint reuses the device-token auth, meters monthly credits in the same D1, and proxies to the model provider with zero data retention (that story has its own chapter). One Worker, one database, one Monday-morning cron that emails a health digest. The monthly bill for all of this infrastructure rounds to zero.
War stories, briefly
- Secrets race deployments. Setting a Worker secret triggers its own deploy; set the webhook secret and immediately fire a webhook, and the first delivery can hit an instance that does not have it yet. Ask us how we know.
- Sandbox and production initially shared a signing key, which meant a sandbox-minted entitlement would verify against production. Caught in review, keys split. Cryptographic material is environment state, not code.
- Anti-enumeration is a discipline, not a feature. Every error path out of the auth endpoints had to be checked for "does this response differ depending on whether the email exists?" It is easy to get right on the happy path and leak on the edge.
None of this chapter is visible in the product, which is the point. The visible part is: buy, type your email, type six digits, done, on as many machines as you own. The invisible part is a tax firm you did not have to become and a licensing server that costs pennies. If you are shipping a paid side project, the boring parts deserve this level of design, precisely so your users never notice them.