Plans and payment¶
roamd has a Free plan and a Paid plan. You can see and change yours on the Plans page.
| Free | Paid | |
|---|---|---|
| Machines | 2 | No limit |
| Terminals open at once | 2 | No limit |
| Activity history | Last 30 days, newest 100 events | Last year, no limit |
| Snippets (saved commands) | No | Yes |
| Sharing with limits (watch only, time limits, per-share permissions) | No | Yes |
The Account page has a Plan card that says which plan you are on and when it renews or ends. It updates when you come back to roamd after paying.
The Plans page shows the paid plan first, with a card below it titled Your current plan. You can pay three ways (side by side on a wide screen, stacked on a phone), listed as monthly, yearly (the best value, shown as a monthly price with the saving) and lifetime (pay once, never renew). A subscription can be cancelled any time; it then stays on until the end of the period you paid for. A lifetime plan has no end date.
Switching to lifetime. On a monthly or yearly plan, Switch to lifetime buys the lifetime plan. Your subscription then stops renewing: you keep what you already paid for, and nothing more is charged.
Paying twice by mistake can't happen. If you press Choose plan again, or in another tab, you get the same payment page. Picking a different option closes the first page before the new one opens.
After paying, the Plans page says it is confirming your payment. Usually that takes a moment. If it takes longer, the page says so and keeps checking. Some payment methods (such as bank debits) take days to clear: the plan starts once the money has arrived.
Refunds. If a lifetime purchase is refunded in full, or your bank takes the payment back, the lifetime plan ends. If roamd wins the dispute, it comes back.
You never give roamd your card¶
You pay on a page run by Stripe, a payment company. roamd never sees your card number, and never sees your data either: end-to-end encryption is the default. The same goes for changing or cancelling your plan: Manage plan opens Stripe's own page.
You roamd Stripe
--- ----- ------
[Buy] -------> "send them to Stripe" --> payment page
you pay there
Plans page <---------------------------- "something changed"
roamd asks Stripe ----> "here is what they
what is paid for <---- are paying for"
and marks the account
roamd always asks Stripe what you are paying for, instead of trusting each message as it arrives. Stripe's messages can arrive late or out of order, and this way a late one can never switch your plan off by mistake. If roamd can't reach Stripe just then, Stripe sends the message again later.
Whoever runs roamd tells Stripe which messages to send: payments, slow payments clearing, plan changes, refunds and disputes. The full list is in the Technical tab.
What Stripe and roamd each know¶
- Stripe knows what you buy and your payment details, as with any online purchase. For your first purchase roamd also hands Stripe your account's email address, so it is already filled in on the payment page (you don't type it again). Stripe sends your receipt there.
- roamd keeps: your account, your plan, Stripe's reference number for you, the date your plan renews or ends, and, for a lifetime plan, Stripe's reference for that one payment (so a refund of it can be matched). It does not keep your card or what you paid with.
- Neither learns anything about your terminals, sessions or files.
When your plan ends¶
If your paid plan ends while you have more than 2 terminals open, you are cut back to 2 at once. The 2 you used most recently stay open. The others show "Your plan ended. Renew to open more than 2."
Nothing running is lost. Only the page's connection is closed. The tmux session keeps running on your computer, and you can open it again (once there is room, or after renewing).
A free account that already has 2 terminals open is told so when it tries to open a third: close one, or choose the paid plan.
Activity history¶
Activity shows what happened on your account. A free account sees the last 30 days and its newest 100 events. A paid account sees the last year, with no limit on how many.
When a plan ends, events beyond the free amount are dropped as new ones are recorded, so renew before then if you want to keep a long record.
Machines¶
A free account can use 2 machines. Pairing a third is refused with a message saying why: remove one, or choose the paid plan.
If your paid plan ends and you have more than 2 machines, the 2 you paired first stay usable. The others are not deleted: they stay paired and listed, but opening one says "Free accounts can use 2 machines…". Renew and they work again straight away.
Plans are per person. Prices include or leave out tax as the Plans page says.
Moving parts¶
browser hub Stripe
------- --- ------
Plans page
buy (month|year| -> one open Checkout session ------> (hosted payment page)
lifetime) per account (reused / expired) |
<---- redirect to its address |
... pays on Stripe ... |
<------ signed webhook ------------+
verify signature + age
list the customer's subscriptions -> Stripe API
<---------------------------------- current state
update the account's plan
(failure -> error reply -> Stripe retries)
if now free: close extra terminals
manage ---------------> create a portal session ----------> (hosted portal)
- Card data never touches roamd. The hub creates Stripe Checkout and customer-portal sessions server to server and hands the browser only the address to go to.
- What the hub stores (one row per account that has bought): the account, the plan
(
freeorpaid), Stripe's customer id, the end of the paid period, whether it is set to cancel, and for lifetime the PaymentIntent id that bought it and whether it was taken back. No card, invoice or price history. - One payment page per account. The hub keeps, in memory, each account's open Checkout
session (made with
expires_atjust over Stripe's 30-minute minimum). A second request for the same choice gets the same address; another choice first expires the open session. If Stripe refuses to expire it (it has been paid and the webhook hasn't come yet), the request is refused with "still being confirmed". Creation is serialised, so two requests at once can't both create one. A completed checkout forgets the session. - What the hub sends Stripe: the account's opaque id (so the webhook can find the
account), the chosen price, the return addresses, and either the customer id (someone who
has bought before) or, for a first purchase, the account's email as
customer_email, so Checkout's form is pre-filled. Never a password or anything about terminals or files. - The Account page's plan card reads the same plan answer as the Plans page. It is fetched when the Account or Plans page is shown and again whenever the tab becomes visible, so a payment made elsewhere shows when you return.
The webhook¶
Stripe calls the hub when something happens. Before anything in the body is read:
- The
Stripe-Signatureheader must carry a timestamp and an HMAC-SHA256 of the timestamp and the exact body, made with the webhook signing secret. It is compared in constant time. - The timestamp must be within a few minutes of now, so a captured request can't be played back later.
A request failing either check changes nothing.
Ordering. Stripe does not deliver events in order and retries failed ones for days. So no
subscription event is trusted as it stands: each one makes the hub list the customer's
subscriptions (GET /v1/subscriptions?customer=…&status=all) and set the plan from that list.
Paid while any subscription is active, trialing or past due. The end date is the latest paid
period end, read from the subscription or, in newer API versions, from its items; a
subscription with cancel_at ends then. "Set to cancel" means every paid subscription is set to
end. An old subscription's late "deleted" therefore can't end a newer one, and a late
"incomplete" can't undo a payment.
Failures are retried. If Stripe can't be reached the hub answers 503, and if the plan can't be saved it answers 500; Stripe then sends the event again. An event for an account that doesn't exist is answered 200 and ignored.
| Event | Effect |
|---|---|
| Checkout completed (subscription) | Customer id kept; plan set from Stripe's subscription list |
Checkout completed (one-off, payment_status paid) |
Lifetime, no end date; the PaymentIntent is kept. Any subscription still renewing is set to cancel_at_period_end |
| Checkout completed (one-off, not yet paid) | Customer id kept; nothing else until checkout.session.async_payment_succeeded |
| Async payment succeeded | As a paid one-off checkout |
Any customer.subscription.* |
Plan set from Stripe's subscription list |
charge.refunded with refunded: true (full refund) |
If it is the lifetime PaymentIntent: lifetime ends, plan set from the subscription list |
charge.dispute.created |
The same |
charge.dispute.closed with status won |
Lifetime given back |
A lifetime account is never changed by subscription events. A lifetime payment that was taken back is not restored by a late copy of its checkout event. A subscriber may buy lifetime (monthly or yearly can't be bought while paid). Any other event is accepted and ignored.
event --> signature ok? --no--> 400
|yes
subscription / checkout / refund / dispute
|
ask Stripe for the subscriptions --fails--> 503 (retried)
|
save the plan --fails--> 500 (retried)
|
200; if now free, close extra terminals
Terminal limit¶
The hub cannot see what is in a terminal, so it counts open connections per account. A free account opening a terminal beyond the limit is refused before the connection starts, with a message saying why. The page also checks, so it can explain before asking.
For each of an account's own terminals the hub notes when the browser last sent anything
(typing, resizing, a reply). Only the time is noted, never contents. When a plan ends,
the terminals used least recently beyond the limit get a close message with the reason
plan-ended, which the page turns into the text above. A guest's terminals on someone else's
machine are not counted against the guest.
Closing a connection leaves the machine alone: the tmux session and anything running in it carry on, and the page can join it again.
Machine limit¶
The hub counts the machines paired to an account. Pairing a new one past the limit is refused. The limit is on machines you pair, not on saved details in your vault: the vault is sealed in your browser, so the hub could not count what is in it, and a machine's saved details only exist for a machine you have paired anyway.
After a plan ends, machines past the first two (in the order they were paired) are refused when a terminal is opened, with the same message. Nothing is removed, so renewing restores them. Taking over a machine of the same name (re-pairing it) does not count as a new one.
Activity limits¶
Activity events are kept per account with an age and a count. Free: 30 days, newest 100. Paid: one year, no count. Failed sign-in attempts keep their own small pool on every plan, so they can never push out real events. The age is applied when events are read and again when one is recorded, so a plan that ends loses the older rows at the next recorded event. A hub not holding to plans uses the paid amounts.
Where the settings live¶
The Stripe keys, the webhook signing secret and the two price ids are read from the hub's
environment file only (the lifetime price is optional; without it that choice isn't offered). They are never logged and never put on a command line. With no key
set, the Plans page says plans aren't on sale yet and nothing can be bought. A separate
switch (ROAMD_ENFORCE_PLAN=1) turns the limits on; until it is on, every account has the paid
features, so nothing is sold either (the hub logs this at start). Managing an existing plan
still works, so it can be cancelled.
Stripe sends only the events its webhook is set to send. All of these must be ticked:
checkout.session.completed --> subscription or lifetime bought
checkout.session.async_payment_succeeded --> slow lifetime payment cleared
customer.subscription.created -+
customer.subscription.updated +-> plan re-read from Stripe
customer.subscription.deleted -+
charge.refunded --> full refund ends lifetime
charge.dispute.created --> dispute ends lifetime
charge.dispute.closed --> won dispute gives it back
A missing one fails quietly: a lifetime plan paid by a slow method never starts, or a refunded one is never ended.