Selling a desktop app with no accounts
Unwait has no signup, no login, and no user database in the usual sense. Payment email is the identity, a license key is the credential, and lapsed users get a smaller app instead of a locked one. The design, and what real payments taught us that test mode did not.
Unwait has no signup. You download the app, it works for 14 days, and if you pay, Stripe emails you a license key that you paste into Settings once. There is no account to create, no password to forget, and no login screen anywhere in the product. Your payment email is your identity.
This is an old model. Desktop software sold this way for decades before SaaS made accounts the default. What follows is why we chose it in 2026, how the pieces fit, and the edge cases that only showed up when real money moved.
Why no accounts
Three reasons, in order of importance.
The trial has to be frictionless. Unwait proves its value on day one or never: it shows cards during AI agent waits, and you either feel the difference the first afternoon or you will not. A signup wall in front of that moment filters out exactly the people we want to reach. So the trial is a local timestamp, created on first launch, with no email asked. Yes, a determined person can game a local trial. That risk is priced in; the kind of user who resets trials was never going to pay, and the users who would pay should not pay friction for them.
One person runs this. Accounts mean password resets, session bugs, email verification, deletion requests, and a user table that grows liabilities faster than value. A license key system is small enough to hold in your head: a key is issued, a key is checked, a key is active or it is not.
There is less to breach. We store what Stripe already has, an email and a subscription state, and nothing else. No passwords, no OAuth tokens, no personal profile. The most boring possible data.
How the pieces fit
The whole flow is five steps:
- Checkout happens in the browser via Stripe Checkout. The app never touches card data.
- A webhook on our server records the subscription and generates a license key.
- The key is emailed to the payment address. That email is the identity and the recovery path.
- The user pastes the key into Settings once. Activation registers the device; a key covers three.
- Cancels and card changes go through the Stripe Customer Portal, so we never built billing UI.
Lost the key? Enter the payment email on the manage page and get status and key back. Every support flow reduces to "the email Stripe has", which is the point: Stripe is the account system, we just do not call it one.
Why keys exist at all, given no accounts: Unwait's card generation runs through our server (users do not bring their own API keys), so something has to say "this install may generate". The license key is that credential and nothing more.
Design for the unhappy paths
Two decisions here did more for the product than any feature.
A lapsed subscription shrinks the app, it does not kill it. When a subscription ends, Unwait drops into starter mode: everything still works, but card generation and news stop, and the built-in starter decks keep serving. The app on your machine stays useful enough to remind you why you paid. Lock a lapsed user out entirely and the uninstall follows within the hour, taking the win-back chance with it.
Offline never punishes a paying customer. The app confirms key status with the server every six hours, and tolerates three days of failed checks before doing anything about it. A dead hotel Wi-Fi or a server hiccup on our side should never downgrade someone who paid. The grace window costs us nothing measurable and removes the worst possible failure mode, which is a paying user staring at a disabled app.
What real payments taught us
We tested the whole chain in Stripe test mode and it all passed. Then we ran the real thing, a live card, a real cancel, real refunds, and found what test mode had not shown.
Portal cancels do not look like you think. Our code watched for cancel_at_period_end: true, the field every tutorial mentions. A real cancellation through the Customer Portal arrived instead with cancel_at set to a concrete timestamp plus canceled_at, and our Settings screen cheerfully displayed a canceled subscription as "Renews on September 4". Handle both shapes, and prefer cancel_at for the end date. We only caught this because a human actually clicked cancel on a real subscription.
Exercise the refund path before a customer does. We issued three real refunds from the Stripe dashboard just to watch our charge.refunded webhook fire. It deactivated the keys immediately, which is what the code said it would do, but "the code says so" and "we watched it happen" are different levels of sleeping well. The refund policy itself is deliberately manual: easy to request by email, approved by a person. Self-serve refund buttons invite abuse, and at indie scale a human reply is faster anyway.
Users do not paste keys the way you issue them. Keys arrive with stray whitespace, in the wrong case, wrapped by email clients. Normalize on input. And when someone hits the three-device limit, the error message includes how to reach us, because a hard wall with no door generates the angriest email of all.
What we gave up
Honesty about the trade: without accounts there is no settings sync between machines, no team plans without extra design, and a key can be shared with a friend inside the three-device limit. All true, all fine. Each of those problems has a price, and at our scale that price is higher than the loss. The moment that math flips is the moment to add accounts, and starting without them does not block adding them later.
The whole billing system described here is a few hundred lines plus Stripe. Breakeven for the product is roughly seven subscribers. That is the real argument for this model: it makes the smallest viable business actually viable, because the infrastructure cost of taking money stays near zero.
If you want to see how it feels from the outside, the trial is the demo: install to first card in under a minute, and nobody asks for your email.