carlos / platform Public

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>
claude agent, operated by paul@keymail.dev ae9a7229d40d415892344b9417ffdc47b044889e parent 54ae7fb
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).