spec: plans, caps and packs for the first paid launch (draft)
Free and Personal plans, packs as licensed quantities, caps with warning, grace and after-grace stages, abuse guards, and the harvest list from the unmerged integration branch. No metered billing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+532
−0
docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md+532 −0
CI cancelled — superseded by a newer push — run details
CI log
=== gate === cd launchctx && go vet ./... && go test ./... -count=1 ok amadan.net/carlos/platform.git/launchctx 0.011s ? amadan.net/carlos/platform.git/launchctx/controlname [no test files] ? amadan.net/carlos/platform.git/launchctx/wireid [no test files] go vet ./... gofmt -l .: clean go test ./... -count=1 -timeout 30m amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md b/docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md| new file mode 100644 |
| index 000000000..72cbeb8b3 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md |
| @@ -0,0 +1,532 @@ |
| +# Plans, caps and packs: getting paid plans live on carloku.com |
| + |
| +Status: draft for Paul's review, 2026-10-20. Supersedes the scope (not the |
| +settled policies) of the billing handoff in amadan discussion #321 for the |
| +first paid launch. |
| + |
| +## 1. Why |
| + |
| +carloku.com charges nobody today. The previous billing effort set out to |
| +bill metered usage (CPU hours, egress, durability writes, storage |
| +byte-months) and to prove every number on an invoice. That grew to 370 |
| +unmerged commits on `feat/carloku-backend-integration-20261003` |
| +(`165472b8`): about 54k lines of product code, 34 files conflicting with |
| +main, and a blocked unit with an unexplained test failure. None of it is |
| +live. |
| + |
| +Paul's direction (2026-10-20): get plans live first, worry about overages |
| +later. The shape that makes that possible: |
| + |
| +- **Charge for counts the console already knows, never for measured |
| + usage.** An app count, a pack quantity, a plan. Nothing on an invoice |
| + needs proving because nothing on an invoice was measured. |
| +- **Measure only to enforce.** Sizes, sends, bytes and awake hours are |
| + still measured, but only to warn and to stop. A measurement that is an |
| + hour late means someone hits a wall an hour late. Nobody is |
| + overcharged. |
| + |
| +The moment we are building for: someone has tried Carloku, deployed a free |
| +app, and wants another. They hit the free limit, and they pay to lift it, |
| +in the console, there and then. |
| + |
| +## 2. Scope |
| + |
| +Two deliveries, in this order: |
| + |
| +1. **Pricing page** on carloku.com showing Free and Personal, rendered from |
| + an approved price book. Can ship before checkout works. |
| +2. **Plans, caps, packs and checkout** in the platform: a free plan, one |
| + paid annual plan (Personal) bought through Stripe Checkout, packs sold |
| + as quantities on the same subscription, caps with warnings and grace |
| + periods, and the abuse guards in section 8. |
| + |
| +### Goals |
| + |
| +- A new account gets the free plan and its caps, with no card. |
| +- A free account that hits a cap can buy Personal (and packs) in the |
| + console and is lifted immediately on payment. |
| +- Every cap warns at 80%, allows a grace period, then refuses only the |
| + capped action. Apps keep serving. Data is never deleted by a cap. |
| +- Existing finite Carloku accounts stay sponsored: they act as paid, at |
| + no charge (settled policy, #321). |
| +- Plans are a general platform feature. A deployment with no plan catalog |
| + configured (Tito, self-hosted boxes, dev unless switched on) behaves |
| + exactly as today: no caps, no billing. |
| + |
| +### Non-goals (later, not never) |
| + |
| +- Metered usage billing and overages of any kind. |
| +- Team plans priced per member (Community, Business). |
| +- Affordability bands (see 9.3). |
| +- Seats as a sellable pack. |
| +- An always-on option on any plan. |
| +- Billing storage requests, replication writes or egress by volume. |
| + |
| +### Anti-goals (would make this a failure even if it works) |
| + |
| +- A surprise bill. Nothing is charged that the customer did not choose. |
| +- Losing a customer's data because of a cap or a lapsed payment. |
| +- A free tier that can be farmed without limit, which makes every cap |
| + meaningless. |
| +- Carloku-specific code paths in the platform. carloku.com is a |
| + deployment that configures plans; it gets no special case. |
| +- Re-importing the old branch wholesale, or its review process. |
| + |
| +## 3. Decisions already made |
| + |
| +By Paul in this conversation (2026-10-20), not to be reopened by review: |
| + |
| +- Scope is (1) the pricing page and (2) flat plans plus packs. Overages |
| + wait. |
| +- Free: **3 apps**. Free apps **sleep after 1 to 2 minutes idle**. |
| +- Paid apps **stay awake up to 15 minutes** after the last request. |
| +- **All apps hibernate.** That is what makes the economics work. No plan |
| + offers always-on. |
| +- **Packs** raise caps: extra apps, storage, email, bandwidth. They are |
| + bought, not metered. |
| +- **Every cap has a grace period with fair notice** before anything is cut |
| + off. |
| +- **Bandwidth** follows the same cap pattern. |
| +- Abuse guards: account farming, things that never sleep, and counting |
| + everything that runs are in v1. Phishing defences are in v1 if free and |
| + paid apps share a domain. Outbound abuse starts with an audit. Hidden |
| + costs start as monitoring. |
| + |
| +Settled earlier and still binding (#321 and the 2026-10-01 rulings): |
| + |
| +- Existing finite accounts are sponsored; sponsorship stays with the |
| + account through ownership transfer; once limits apply, exhaustion pauses |
| + safely and data is kept at Carloku's expense. No automatic charging. |
| +- `console.carloku.com` is the paid deployment. Tito and other |
| + self-managed deployments stay billing OFF. No hostname-triggered |
| + activation. |
| +- Three currencies, lower case: usd, eur, gbp. Default by billing country |
| + (EU member states eur, GB gbp, everything else usd), with a visible |
| + override. An explicit currency we do not sell is refused, never |
| + defaulted. Nothing is converted. |
| +- A live console refuses a draft price book; approval is a new book |
| + version, never an edit. |
| +- Stripe keys are restricted `rk_` keys in the console's EnvironmentFile, |
| + never the bucket, never `carlos secrets set`. |
| + |
| +## 4. Plans and limits |
| + |
| +### 4.1 The catalog |
| + |
| +A deployment opts in by configuring a **plan catalog**: a versioned file |
| +(shape left to the builder; JSON beside `internal/plans` is the obvious |
| +home) that names each plan's caps and each pack's increment. No catalog |
| +configured means no caps and no billing. |
| + |
| +An account's **effective limits** are its plan's caps plus, for each pack, |
| +quantity times increment, never above the pack's ceiling. The console |
| +computes them. Every enforcement point reads effective limits, never a |
| +plan name. |
| + |
| +### 4.2 Numbers |
| + |
| +Paul's numbers are marked **(Paul)**. Everything else is a proposal for |
| +Paul to approve or change before the book is approved; the builder treats |
| +them as configuration, not constants. |
| + |
| +| Limit | Free | Personal | Pack adds | Hard ceiling | |
| +| --- | --- | --- | --- | --- | |
| +| Running things (apps + instances, 5.1) | 3 **(Paul)** | 25 | +5 | 200 without an operator | |
| +| Idle window before sleep | 90 s (1 to 2 min **(Paul)**) | 15 min **(Paul)** | none | 15 min | |
| +| Memory / CPU per app | 256 MB / 0.5 CPU | 512 MB / 1 CPU (today's default) | none | the existing 1 GB / 2 CPU children tier | |
| +| Awake hours per month (account) | 100 | 1,500 | none in v1 | n/a | |
| +| Database + files on box (per app) | 1 GB | 10 GB | +10 GB (account pool, see 5.4) | 2x cap, and box safety | |
| +| Object storage (per account) | operator-approved only, 1 GB if approved | 25 GB self-serve | +50 GB | 2x cap | |
| +| Email per day (account) | 50 | 1,000 | +1,000 | 10,000 without an operator | |
| +| Bandwidth per month (account) | 10 GB | 200 GB | +200 GB | 2x cap, then throttled | |
| +| Free accounts per person | 1 | n/a | n/a | n/a | |
| +| Custom domains | none | allowed | n/a | n/a | |
| +| Point-in-time database history | latest copy only | 7 days | none | n/a | |
| + |
| +Prices: Personal at the draft book's annual bases (usd 28, eur 24, gbp 22). |
| +Pack prices are Paul's to set; the book needs one price per pack per |
| +currency. |
| + |
| +## 5. The caps |
| + |
| +Every cap has the same three stages (section 6). This section says, per |
| +cap, what is counted, where it is enforced, and what "refused" means. |
| + |
| +### 5.1 Running things (the app cap) |
| + |
| +**Counted:** apps, temporary apps, and customer instances (including |
| +seeded and imported). Each counts as one. Sidecar workers do not count |
| +against the cap, but a free account may run at most one sidecar per app, |
| +and sidecars obey the plan's idle window (5.2). Fleets are customer-owned |
| +boxes, not apps; free accounts cannot create fleets. |
| + |
| +**Enforced at:** |
| +- `claimAppWith` (`internal/console/server.go:4052`), the single claim |
| + check both create doors and temporary apps already use. The temporary |
| + app cap (`apitemporary.go:24,66,167`) is the template: checked under |
| + `lockAccount`, refused with 409 and `"code":"cap"`. |
| +- `restoreApp` (`restore.go:55`) and the claims-mode restore |
| + (`claim_restore.go:120`), which do not pass through the claim. |
| +- `provision.CreateRecord` (`internal/provision/provision.go:882`) for |
| + instances, reached from `apiinstances.go:1664`, `apiinstanceseed.go:341`, |
| + `apiinstanceimport.go:172` and `domainattach.go:237`. |
| +- Direct mode skips the claim (`server.go:~4030`). The builder must either |
| + count those apps when their rows are filled from the bucket and apply |
| + the over-cap stages, or refuse direct-mode creation for plan-governed |
| + accounts. Leaving it open is not an option. |
| + |
| +**Over the cap:** creation is refused. Nothing that already runs is |
| +touched. An account can be over the cap only by losing a plan or a pack |
| +(lapse, downgrade, reduction at renewal). Then: grace (section 6), and |
| +after grace the **newest** running things beyond the cap stop serving (a |
| +suspended state with a clear page) until the account is back under the |
| +cap or buys more. Nothing is deleted. |
| + |
| +### 5.2 Idle window, and things that never sleep |
| + |
| +**Idle window.** Today `cfg.Idle` is box-wide (`CARLOS_HIBERNATE_IDLE`, |
| +default 15 min, `internal/activator/activator.go:101,123`), and only |
| +sidecar workers carry a per-route window (`rt.IdleSeconds`, |
| +`activator.go:3114`, used at `:4646`). The plan's idle window reaches the |
| +box as a per-route `IdleSeconds` set by the provision reconcile pass |
| +(`internal/provision/reconcile.go:441` for instances, |
| +`sidecar_reconcile.go:144` for sidecars) from the account's effective |
| +limits, and the activator honours a per-route window for every entry, not |
| +only workers. A route's window is the smaller of its own and the plan's. |
| + |
| +**Never sleeping is refused for plan-governed accounts:** |
| +- the `daemon` capability (`internal/caps/caps.go:106`), refused at |
| + release promote/ship time with a message naming the plan, not just at |
| + converge; |
| +- `SidecarManifest.Hibernate = false` (`provision/sidecar.go:56`, |
| + member-settable at `apisidecars.go:614`); |
| +- any member-reachable path that yields an always-on route. Address-proxy |
| + routes default to always-on (`provision.go:444`); the builder confirms |
| + whether a member can create one and closes it if so. |
| + |
| +Operators keep `carlos route --hibernate=false` for their own needs. |
| + |
| +**Schedules.** `MinEvery` is one minute (`internal/schedule/spec.go:21`) |
| +and a due tick holds the host awake (`activator.go:~4655`). Free accounts |
| +get a minimum schedule interval of 15 minutes, validated where `ValidEvery` |
| +runs. Paid keeps one minute. |
| + |
| +**Awake hours.** A monthly awake-hours cap per account backstops keep-alive |
| +pingers, open WebSockets and mining. The source is the per-account |
| +`instance_hours` figure the hourly economics pass already writes to |
| +`usage/accounts/<acct>/<month>.json` (`economicspass.go:378-397,835`). It |
| +is approximate and that is acceptable: it only gates. After grace, the |
| +account's apps get the shortest idle window the platform allows (and |
| +WebSocket upgrades are refused) until the month rolls or the account |
| +upgrades. They still wake and serve. |
| + |
| +### 5.3 Memory and CPU |
| + |
| +Add a third tier beside `TierDefault` and `TierChildren` |
| +(`internal/cgroup/cgroup.go:92`): 256 MB, CPU quota 50%. `memTier` |
| +(`internal/host/memtier.go`) chooses it from the route's account limits. |
| +The tier lists that hard-refuse unknown tiers need the new entry: |
| +`strict_linux.go:216`, `strict_gen_linux.go:162`, `gen.go:65`, and |
| +unit-backed apps' numbers in `caps/render.go`. |
| + |
| +There is no grace stage: a ceiling is a ceiling. What is new is the |
| +**explanation**: when an app is OOM-killed under the free tier, the |
| +console's app page and `carlos` CLI status say so in plain words and |
| +point at the upgrade. A free app that cannot run in 256 MB should fail |
| +loudly and once, not restart-loop silently. |
| + |
| +### 5.4 Database and files on the box |
| + |
| +Today nothing stops one app filling a box's disk (single gp3 root volume, |
| +`infra/modules/region-host/main.tf:493`; data at `/data/<host>.db` and |
| +`/data/instances/<id>/`, `provision.go:2912`, `bootstrap.go:75`). This is |
| +a live risk whether or not anyone pays, so it is in v1. |
| + |
| +**Enforcement:** a per-app quota on the app's data directory, enforced by |
| +the kernel, so writes fail at the hard limit with no polling and no race. |
| +Filesystem quotas fit the policy exactly: a soft limit with a grace timer |
| +and a hard limit. The builder chooses the mechanism (ext4 project quotas, |
| +an XFS data volume with project quotas, or another), and the choice may |
| +mean new boxes get a data volume built for it. Existing boxes must keep |
| +working without a quota until they are replaced; a box without quota |
| +support falls back to a periodic size check that refuses wakes after grace. |
| + |
| +**Counting for warnings:** a periodic size check per app (the quota's own |
| +usage report where available) feeds the 80% warning and the grace clock. |
| + |
| +Per-app cap or account pool: the cap is per app, and a storage pack raises |
| +the cap of every app on the account. (Proposal; the simpler rule to |
| +enforce in the kernel.) |
| + |
| +**After grace:** writes fail (SQLite sees a full disk). Reads keep |
| +working. The app page says why. |
| + |
| +### 5.5 Object storage |
| + |
| +Free accounts keep today's rule: an operator approves storage |
| +(`internal/console/apistore_entitlement.go`, `objstore/entitlement.go`). |
| +Paid accounts get it self-serve up to their cap. |
| + |
| +**Counting:** `sizeProvisionedStore` (`economicspass.go:1023`) already |
| +lists each store bucket's bytes daily into `usage/storage/<date>.json`. |
| +Daily is enough. |
| + |
| +**After grace:** a `Deny s3:PutObject` statement in the store's policy |
| +(`storePolicyJSON`, `internal/objstore/aws.go:180`; `EnsureUser` rewrites |
| +the whole inline policy). Reads and deletes keep working, so the customer |
| +can clean up. The MinIO adapter needs the equivalent (section 10). |
| + |
| +Downloads straight from a store do not pass the edge, so they are outside |
| +the bandwidth cap. Storage request counts are monitored, not capped, in |
| +v1 (8.6). |
| + |
| +### 5.6 Email |
| + |
| +The mechanism exists: daily caps by tier (`internal/console/mailcounts.go:67`), |
| +counted from SES events, enforced by pausing the sender's key |
| +(`mail/walk.go:1120`) until the UTC day rolls. What is missing is the plan: |
| +`mailBillingTier` always answers `"free"` (`apimail.go:245`, #226). It |
| +reads the account's effective email limit instead. |
| + |
| +Email's cap resets daily, so a week-long grace does not fit. Its grace is |
| +**within the day**: warn at 80%, allow up to 2x the cap, pause at 2x. The |
| +10,000 a day hard ceiling is not raised by packs; only an operator raises |
| +it, because sending reputation is shared by every tenant. |
| + |
| +### 5.7 Bandwidth |
| + |
| +**Counting:** the edge already counts response bytes per route |
| +(`internal/edge/usagemeter.go:36`), flushed every 5 minutes into |
| +`usage/edge/<box>/days/<date>.json` and summed per account per month as |
| +`egress_gb` by the economics pass (`economicspass.go:409-427`). Approximate |
| +and late by up to an hour: acceptable for a gate. |
| + |
| +**Gap:** WebSocket traffic after the upgrade is not counted |
| +(`edge.go:1704`). v1 accepts that and relies on the awake-hours cap. |
| + |
| +**After grace:** throttle, do not refuse. The account's routes are served |
| +at a reduced rate until the month rolls or the account upgrades. This |
| +needs a per-account throttle state that every edge can see; it travels in |
| +the account's limits record (section 7). Grace for a monthly cap is "the |
| +rest of the month, up to 2x the cap". |
| + |
| +## 6. Warnings, grace and notice |
| + |
| +One engine, used by every cap except memory: |
| + |
| +1. **Warning at 80%.** Console banner on the account and the affected |
| + app, a warning line in `carlos` CLI output for commands touching the |
| + account, and an email to the account's owners. |
| +2. **Over the cap, in grace.** Everything keeps working. A daily reminder |
| + says what is over, by how much, when grace ends, and the three ways |
| + out: buy a pack, upgrade, or clean up. |
| +3. **After grace.** Only the capped action is refused (per cap, above). |
| + The app keeps serving. Data is untouched. |
| + |
| +Grace periods: 7 days for counts and sizes (apps, disk, storage); |
| +"the rest of the day up to 2x" for email; "the rest of the month up to 2x" |
| +for bandwidth and awake hours. Every grace has a hard ceiling behind it |
| +(table 4.2): grace never means unlimited growth. |
| + |
| +State lives in the console (one row per account per cap: first over, |
| +grace ends, last notified), so a reminder is sent once per period and a |
| +console restart does not reset a clock. |
| + |
| +**Dependency:** the console cannot email account owners today; giving it |
| +an SES sending grant is an open security ruling (the same one holding |
| +console invite delivery, see memory `console-invite-delivery`). Until it |
| +is settled, warnings go to the console and CLI only, and v1 must not |
| +start an after-grace stage for a cap whose owners could only have been |
| +told by email. |
| + |
| +## 7. How limits reach the boxes |
| + |
| +The console is the single writer of an account's effective limits. It |
| +writes them to one record in the bucket per account; boxes read it the |
| +same way they already read `accounts/<sqid>/account.json` |
| +(`domains/reconcile.go:573`, exposed to the account's own boxes by |
| +`edge/storedoor.go:376`). |
| + |
| +The integration branch's `internal/entitlement` record |
| +(`entitlements/accounts/<sqid>.json`, with |
| +`Runtime{IdleSeconds, MaxApps, MaxMembers, MemoryTier, ...}`) is the shape |
| +to start from. Harvest the record; leave its writer-lineage ("authority") |
| +machinery behind unless the builder finds a second writer, which v1 should |
| +not have. |
| + |
| +Boxes use the record for: idle window, memory tier, throttle state, |
| +suspended routes, and the disk quota size. The console uses its own |
| +database for everything it enforces itself (creation caps, email, |
| +storage policy, custom domains, schedules). |
| + |
| +A box that cannot read the record keeps the last one it read. A box that |
| +has never read one for an account applies **no** caps (fail open), and |
| +reports it. Caps protect economics, not security; failing closed would |
| +take customer apps down on a bucket hiccup. |
| + |
| +## 8. Abuse guards |
| + |
| +1. **One free account per person.** `mintAccount` (`server.go:2226`) is the |
| + path for every console account creation (auto-mint on first sign-in, |
| + `/accounts/new`, the CLI). A person may own one free account. More |
| + accounts are created as paid (checkout first) or by an operator. |
| + `carlos accounts create --owner` (operator, bucket-direct) is exempt. |
| + Address farming is expected; detecting farms is later, not v1. |
| +2. **Never-sleep paths refused** (5.2). |
| +3. **Everything that runs counts** (5.1). |
| +4. **Phishing and malware.** v1 keeps one apps domain for free and paid |
| + (a separate free domain would rename apps on every upgrade; see 9.2). |
| + Because they share it, v1 ships: custom domains for paid accounts only |
| + (gate in `attachDomainAs`, `domainattach.go:116`, above the uniqueness |
| + checks per that file's ordering rule); an abuse-report address on the |
| + site and in each app's footer page; and an operator takedown command |
| + that suspends an app and its account's other apps with a reason. |
| +5. **Outbound abuse.** Units already deny private ranges and metadata |
| + (`IPAddressDeny`), but the public internet is open and nothing blocks |
| + outbound port 25. v1: block outbound SMTP (25) from tenant units on |
| + plan-governed deployments, and write down what else is open. Further |
| + egress policy is later. |
| +6. **Hidden costs, monitored.** Per-app wakes per day (a 90-second window |
| + can turn steady light traffic into a sleep/wake cycle every few |
| + minutes; wakes reuse the warm local database, `backend_exec.go:784`, |
| + but still take a lease and a checkpoint), replication request rates, |
| + store request counts, and ship frequency (the staging disk is never |
| + reaped, amadan #122). Reported per account on the operator economics |
| + view. No caps in v1. |
| +7. **Sponsored accounts have limits too.** "Cover current usage" means a |
| + sponsored account's effective limits are the larger of Personal's caps |
| + and its usage on the day plans switch on, recorded once. Increases are |
| + reviewed separately (settled policy). |
| + |
| +## 9. Checkout and packs |
| + |
| +### 9.1 What gets sold |
| + |
| +One subscription per account, annual, in the account's currency: |
| + |
| +- one item: Personal, quantity 1; |
| +- zero or more items: one per pack type, quantity = number of packs. |
| + |
| +Adding a pack mid-year updates the item's quantity with Stripe proration. |
| +Reducing a pack takes effect at renewal. Upgrading from free is Stripe |
| +Checkout in subscription mode. Managing payment method and invoices is |
| +the Stripe customer portal, locked to those functions. The portal does not |
| +change plans or quantities; the console does. |
| + |
| +### 9.2 Lapse and downgrade |
| + |
| +A failed renewal follows Stripe's retry schedule. While Stripe retries, the |
| +account keeps its paid limits. When the subscription ends, the account |
| +drops to free limits and section 6 applies to every cap it is now over, |
| +with its normal grace. Nothing is deleted. |
| + |
| +### 9.3 Bands |
| + |
| +The original model scaled price by an affordability band (four bands per |
| +plan). v1 sells at one price per currency (band A). Bands multiply the |
| +catalog by four and thread through checkout metadata, intents and |
| +records; they can be added later as more prices without changing the |
| +model. (My call; Paul to confirm.) |
| + |
| +### 9.4 Code to harvest from `165472b8` |
| + |
| +Not a merge. Copy onto current main, adapt, and review as new code. |
| + |
| +- **Copy:** `internal/billingstripe` without `invoices.go` (stdlib only, |
| + idempotency on every write, webhook signature verification with |
| + rotation, portal, customer, catalog checks); `internal/entitlement` |
| + (minus authority lineage). |
| +- **Adapt:** `internal/plans` down to one plan plus packs per currency, no |
| + bands or countries, keeping the draft/approved gate and |
| + `PublicPriceBook`; `cmd/console/billing.go` env handling with new price |
| + keys (`<book>/personal/<cur>`, `<book>/pack/<name>/<cur>`); the core of |
| + `billingstore` (accounts, checkout intents, book assignments, event |
| + dedupe, rejected events, reviews), `billingservice`, `billinghandlers`, |
| + `billingpage`, `billingoperator` (the house flag), `entitlementstore` |
| + (fill limits from plan plus packs); the quantity update and fence from |
| + `billingquantity`, aimed at pack items. |
| +- **Leave behind:** everything metered (`usage` durability, |
| + `resourcebudget`, `awsbilling`, `authority`, invoices, calculation, tax |
| + split, statements, overage preferences, `month.go`, `lines.go`), bands |
| + and countries, the legacy-EUR path, sponsored cohort enrollment and |
| + observation (the house flag covers sponsorship), and |
| + `billingretirement` unless the builder shows v1 needs it. |
| +- **Main rework:** the old code assumes one subscription item everywhere |
| + (`billingservice.go:939`, `billingquantity.go:1015`). Packs mean several |
| + items. Reconciliation, intent matching and price recognition must handle |
| + a set of items, each recognised against the book. |
| + |
| +Tests are rewritten for v1, not copied. |
| + |
| +### 9.5 Open decisions from the old branch that still apply |
| + |
| +From `DECISIONS-FOR-PAUL.md` on the integration branch: 1 to 5 (money on |
| +a refused purchase, what "settled" requires, Stripe contradicting itself, |
| +an unbound checkout, what the buyer sees during review), 6 to 8 recast |
| +for pack reductions, 9 (who is free on switch-on day: answered here, |
| +existing accounts are sponsored, new ones are free), and 15 |
| +(`incomplete_expired`). The builder proposes an answer for each in the |
| +plan; Paul rules. |
| + |
| +### 9.6 Stripe live setup |
| + |
| +Following the old `ENABLEMENT.md` procedure, reshaped: products (Personal, |
| +one per pack type), prices (one per product per currency, yearly, |
| +licensed, tax-exclusive), the 100% house coupon, a locked portal |
| +configuration, the webhook (`checkout.session.completed`, |
| +`customer.subscription.{created,updated,deleted}`, `invoice.paid`, |
| +`invoice.payment_failed`), the restricted key, and the console variables. |
| +IE and OSS tax registrations already exist. Every live step is Paul's to |
| +approve; a test-mode rehearsal on dev comes first. |
| + |
| +## 10. Provider boundary |
| + |
| +- Caps, grace, notices, plan catalog and limits records are platform core: |
| + no AWS or Stripe dependency. A self-hosted deployment can configure |
| + plans with no billing provider at all (an operator assigns plans). |
| +- Stripe is a billing adapter behind the console's billing service. |
| +- Store write blocking is an objstore adapter operation: AWS writes a |
| + Deny statement; MinIO writes its own policy equivalent. |
| +- Disk quotas are a host (Linux) mechanism; non-Linux builds get a stub |
| + (cross-compile gate). |
| +- Outbound port blocking is a host mechanism, Linux only. |
| + |
| +## 11. Pricing page (delivery 1) |
| + |
| +The page lives in the carloku.com site (deployed from `bes/website`, see |
| +memory `platform-hostnames-209`; earlier drafts in the |
| +`carloku-paid-pricing` worktree, rendered from |
| +`copy-review/pricing/strings.json` and `public-pricebook.json`). It shows |
| +Free and Personal from the approved book's public export, the caps table |
| +in human terms, and the packs. The upgrade button goes to the console; |
| +until checkout is live it goes to sign-up. |
| + |
| +Gates: an approved price book (a new version, not an edit of the draft); |
| +copy review of every string (the 2026-09-22 approval is lost and does not |
| +count); the site refuses to publish from a draft book. |
| + |
| +## 12. Rollout |
| + |
| +All live steps need Paul. Dev before flagship (roll order rule). |
| + |
| +1. Ship the plan catalog, limits records and caps **dormant** (no catalog |
| + configured). Nothing changes anywhere. |
| +2. Dev: configure a catalog; mark every existing dev account sponsored; |
| + create fresh free accounts and exercise every cap through grace and |
| + after grace; Stripe test mode end to end, including a lapse. |
| +3. Flagship: mark every existing account sponsored (house) with limits |
| + recorded from current usage. Configure the catalog. New accounts are |
| + free. Watch a week of warnings with after-grace stages disabled. |
| +4. Enable after-grace stages (only for caps whose notice reached owners, |
| + section 6). |
| +5. Stripe live provisioning (9.6), then switch checkout on. Publish the |
| + pricing page's upgrade button. |
| + |
| +## 13. Left to the builder |
| + |
| +- Catalog and limits record file shapes. |
| +- The disk quota mechanism and the data-volume change it needs. |
| +- How throttling is applied at the edge. |
| +- Delivery slicing, beyond the order: limits and records, then caps, then |
| + notices and grace, then checkout, then the page. |
| +- Copy for every refusal and warning (through copy review). |