Guest Checkout (buying without an account)
Guest checkout lets a visitor pay for a configured one-time licence without signing up. Nothing is stored locally until the configured provider confirms payment through a signed event. The durable worker then creates the entitlement and queues a claim email carrying a single-use link that attaches the purchase to a real account. That application email is not the provider-issued invoice or tax receipt.
Disabled by default. Enable it with the server-only GUEST_CHECKOUT_ENABLED environment variable, or answer the prompt in pnpm run init. Turnstile keys are required, because the checkout endpoint is anonymous.
Stripe and Lemon Squeezy implement this licence-only guest flow. Paddle remains fail-closed for guest checkout: its member checkout can accept a late payment only because the Account, actor, quote, and exact transaction were durably prelinked before payment, while an anonymous buyer has no equivalent Account-owned boundary. The initialization script automatically disables guest checkout when Paddle is selected.
Guests may buy configured one-time licences only — licence entries in pricingConfig.products. Subscriptions are refused at the schema boundary: a recurring charge against someone with no login has no self-service cancellation, which is a consumer-law liability rather than a UX gap. Credit packs are excluded too, since purchased credits are unusable until the buyer claims the account.
Where the option appears
The pricing page is unchanged. A visitor picks a licence exactly as before, and the selection is parked in the bsk_pending_checkout cookie. The alternative is offered at the moment the licence asks them to create an account — on /login, as a “continue without an account” button below the sign-in form.
That button only renders when the feature is on and a one-time licence is already parked. A visitor who came for a subscription never sees it, so the affordance cannot lead to a refusal. The check runs server-side and ships as static markup, which means the flag itself never reaches the browser and the button cannot drift from what the API accepts. The customer never sees or chooses a payment provider.
The flow
/pricing (unchanged) → licence parked in the cookie ↓ /login “Continue without an account” ↓ /checkout/guest email + terms + withdrawal waiver + Turnstile ↓ POST /api/billing/guest-checkout ← writes nothing configured hosted payment ↓ signed paid event journaled before processing bounded provider retrieval verifies payment facts and buyer email ↓ atomic RPC creates guest account + payment + licence + claim + email outbox worker sends the single-use claim link /checkout/claim → POST /api/billing/guest-claim
Why nothing is written before payment
The checkout endpoint is anonymous. Creating an account row there would let unauthenticated traffic create unbounded records, and every abandoned checkout would leave an orphan behind. Instead the endpoint only calls the configured provider. After a signed paid event is durably journaled, the worker retrieves and verifies canonical provider facts, then one database transaction creates the guest account, normalized payment, licence, credits, claim and must-deliver email. The account has no owner and no membership, so ordinary business repositories cannot select it — only the claim capability can transfer its entitlement.
Claiming a purchase
The claim link is single-use and expires after 72 hours. The claim table stores only a digest; the raw bearer exists temporarily in the service-only email outbox and is redacted after delivery, expiry, successful claim or replacement. The email link exchanges the bearer for a short-lived HttpOnly cookie before redirecting to a clean browser URL. Redeeming it requires a recent sign-in with the address that paid, then moves the licence, payment record and included credits onto the account the buyer already uses.
The purchase is transferred, not adopted: adopting the guest account would leave the buyer with two personal accounts, and the second one would be invisible to the rest of the product.
The guest and authenticated licence checkout forms collect an explicit, separate waiver of the 14-day withdrawal right for immediate digital delivery (Directive 2011/83/EU, Art. 16(m)). It is deliberately not folded into the terms checkbox — a waiver bundled into a general acceptance is not “express”.
Housekeeping
When guest checkout is enabled, pnpm run init registers two internal jobs automatically. sync-guest-purchases runs every five minutes and continues large verified purchase histories in bounded batches. cleanup-guest-accounts runs daily, deletes guest accounts that hold neither a licence nor a payment, and never deletes an account that still holds a purchase — those rows cascade, so removing the account would destroy a paid entitlement with the money already taken.
The job also reports unclaimed_expired_purchases: buyers who paid and never claimed. A count that keeps growing is the signal that claim emails are not being delivered. Expired lookup authorizations and raw CTA bearers are purged or redacted in bounded batches. Terminal claim and order-lookup emails retain recipient data only for the configured support window; the durable payment, licence and claim facts remain.
Data protection
A claimed guest purchase appears in the account owner’s data export and is removed by the account deletion job. A buyer who never claimed is attached to no authenticated account, so account self-service cannot reach that purchase. Handle the request through support after verifying provider-issued purchase evidence, as described in the privacy notice.