# 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,
  except what a lapsed or reduced paid account no longer pays for, and
  only after grace, a 90-day disabled period and the Trash (9.2).
- 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 without warning, or while they are still
  paying for it.
- 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.
- **After a lapse, paid-only features get a grace period, are then
  disabled, and are eventually deleted.** Agreed details: 7 days grace,
  90 days disabled with reminders, then the Trash's 30 days (9.2).
- 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 running thing, 5.4) | 1 GB | 10 GB | +10 GB to every running thing | 2x cap, and box safety |
| Sidecar workers per app | 1 | 5 | none in v1 | 5 |
| 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 running-things cap; they have their own per-app cap (table
4.2: 1 on free, 5 on Personal), enforced at `handleAPISidecarCreate`
(`apisidecars.go:216`), and they obey the plan's idle window (5.2). Fleets
are customer-owned boxes, not apps; free accounts cannot create fleets.

**Grace and creation (codex 1.2).** Refusing to create something new is
the cap itself, not a cut-off, so it has no grace: the warning at 80%
is the notice. Grace protects what already runs. It applies whenever a
lowered limit would take something away from something running.

**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.
- Instance creation. `provision.CreateRecord`
  (`internal/provision/provision.go:882`) is **not** a common choke point
  (codex 1.10): claims-mode creation goes through `finishClaimCreate`
  instead (`apiinstances.go:1658`). The check belongs in the console
  handlers before either write: `handleAPIInstanceCreate`
  (`apiinstances.go:987`, which the seeded and imported variants pass
  through) and the first-route attach (`domainattach.go:237`), under the
  same account lock as the app claim so the two counts cannot race.
- 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 are disabled
(stop serving, show a clear page), and, if still over, deleted after the
retention period (9.2). This is the one case where a cap leads to
deletion.

### 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.

A shorter idle window cannot stop an app that is never idle: a steady
pinger, a long stream or an open WebSocket keeps `inflight` above zero
(`activator.go:4650`) (codex 1.1). So after grace the account is
**throttled** through the same per-account throttle state as bandwidth
(5.7), its open WebSockets are closed and new upgrades refused, and each
app gets a continuous-awake ceiling (the activator's existing run ceiling,
`MaxRunSeconds`, applied to instances): after 15 minutes awake the app is
put to sleep and wakes on its next request. Apps still wake and serve,
slowly, until the month rolls or the account upgrades.

### 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`.

A new app gets its plan's tier at once. An app that is already running
when its account's limit drops (lapse, downgrade) keeps its old tier
through the 7-day grace and moves to the lower tier at its next start
after grace (codex 1.2). Lowering memory under a running process would
kill it, which is the cut-off without notice the grace rule forbids. 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.

**The unit is the running thing (codex 1.5).** Each app or instance has
its own data directory on exactly one box (`provision.go:2960`), so the
quota is per data directory, and a storage pack raises the cap of every
running thing on the account. This sells more total disk than an account
pool would, but every running thing is already counted and paid for, and
it needs no allocation across boxes.

**Boxes without quota support (codex 1.6).** Existing boxes keep working
without a quota until they are replaced. On those boxes the disk cap is
**warn-only**: warnings and reminders, no after-grace stage, and never a
stop or a refused wake for disk. The box-level disk alert and placement's
10% free-disk floor (`boxchoice/policy.go:22`) remain the protection
there. The disk cap's after-grace stage switches on per box, only where
the quota exists. Replacing flagship boxes with quota-capable ones is a
rollout prerequisite for that stage only, not for launch.

**Counting for warnings:** a periodic size check per data directory (the
quota's own usage report where available) feeds the 80% warning and the
grace clock.

**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

Half the mechanism exists: daily caps by tier
(`internal/console/mailcounts.go:67`), counted from SES events, enforced by
pausing a sender's key (`mail/walk.go:1120`) until the UTC day rolls. But
it is **per sender, not per account** (codex 1.11): `enforceMailLimits`
reads one sender's counts against that sender's persisted grant
(`mailcounts.go:253`), and `mailBillingTier` (`apimail.go:245`, always
`"free"`, #226) is only consulted when a grant is created
(`apimail.go:1297`). v1 needs:

- an account-wide daily count, summed over the account's senders, checked
  against the account's effective email limit; at the limit every sender
  of the account is paused;
- the per-sender cap kept as it is (an operator's `DailyCap` still works);
- existing grants re-derived when the account's limit changes (plan,
  pack, lapse), not only at grant creation.

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, notice received), so a reminder is sent once
per period and a console restart does not reset a clock.

**What counts as notice (codex 1.4).** Notice is received when any of
these is recorded for an owner of the account, after the cap went over:
the warning email was accepted for delivery; the owner loaded a console
page that showed the banner; or the owner ran a CLI command that printed
the warning. **An after-grace stage starts only when grace has ended and
notice was received at least 3 days earlier.** An unattended account
whose owners were never reached stays in grace. Two things do not wait
for notice: hard ceilings (they protect the box and every other tenant,
not our economics) and refusing to create something new.

**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, notice is only the console banner and CLI warning.

## 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.
   The rule applies at creation only (codex 1.8). A person who owns
   several paid accounts that later lapse keeps them all, each on free
   limits: each was paid for, and the alternative means deleting or
   charging. Ownership transfers are not checked against it either.
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, per limit, the larger of
   Personal's cap and its measured usage, recorded once when plans switch
   on (codex 1.9):
   - counts and sizes (running things, disk, storage): as measured on
     switch-on day;
   - recurring figures (email, bandwidth, awake hours): the highest full
     month in the three months before switch-on, from the existing
     monthly usage statements (`usage/accounts/<acct>/<month>.json`);
     email from the per-day counts, highest day.
   A recorded sponsored limit may exceed a table 4.2 hard ceiling; it is
   then an explicit operator-approved figure on that account, listed for
   Paul before switch-on. Increases after that are reviewed separately
   (settled policy). Nothing is invented: an account with no statement
   history gets Personal's caps.

## 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.

**What the account no longer pays for is disabled, then deleted (Paul,
2026-10-20, codex 1.7).** This covers everything a lapse, downgrade or
pack reduction leaves beyond free (or the remaining) limits: running
things beyond the cap and their data, sidecars beyond the cap, attached
custom domains, and fleets. The steps:

1. **Grace, 7 days** (section 6, including its notice rule). Everything
   keeps working.
2. **Disabled.** Stops serving; a custom domain stops routing; a fleet
   takes no new work. Configuration and data are kept. Paying again (or
   buying the pack back) re-enables it at once.
3. **Deleted, 90 days after being disabled**, with reminders 30, 7 and 1
   days before. Deletion goes through the existing Trash
   (`release.TrashCooloff`, 30 days, `internal/release/trash.go:33`), so
   it can still be restored for a month. Data is gone for good about four
   months after the lapse at the earliest.

Which running things are kept is the account's choice where it makes
one before grace ends (pick which three stay); otherwise the newest go
first. Schedules faster than the free minimum are not deleted: after
grace they run at the free minimum interval.

**Sponsored accounts are never on this path.** They keep the settled
policy: paused at exhaustion, data kept at Carloku's expense.

### 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, and `billingretirement` unless the builder shows v1 needs
  it.
- **Sponsorship needs a new path (codex 1.12).** The harvested house flag
  does not cover it on its own: at `165472b8` `queueEntitlementTx`
  (`internal/console/entitlementstore.go:219`) publishes nothing for an
  account with no subscription, and sponsored accounts never check out.
  v1 needs a subscription-independent plan assignment (an operator sets
  an account's plan, with its recorded sponsored limits from 8.7) that
  writes the same limits record. Self-hosted deployments without a billing
  provider use the same path (section 10).
- **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).

## 14. Review record

Round 1, Astra builder check at `5ff0e014`, findings committed unedited as
`CODEX-FINDINGS-PLANS-CAPS-1.md`. Numbering here: questions 1 to 9 are
codex 1.1 to 1.9; the three citation errors, in order, are codex 1.10 to
1.12. All twelve are accepted:

| Finding | Where answered |
| --- | --- |
| 1.1 awake-hours cap cannot stop a never-idle app | 5.2: throttle, close WebSockets, continuous-awake ceiling |
| 1.2 grace vs creation refusal and memory reductions | 5.1, 5.3 |
| 1.3 sidecars unbounded on paid | table 4.2, 5.1 |
| 1.4 what counts as notice | section 6 |
| 1.5 storage unit | 5.4: per running thing |
| 1.6 boxes without quotas | 5.4: warn-only there |
| 1.7 paid-only features after downgrade | 9.2, Paul's ruling: grace, disable, delete after 90 days via the Trash |
| 1.8 one free account per person after lapse | 8.1: creation only |
| 1.9 sponsored "current usage" measurement | 8.7 |
| 1.10 `CreateRecord` is not the instance choke point | 5.1 |
| 1.11 email caps are per sender | 5.6 |
| 1.12 house flag publishes no limits without a subscription | 9.4 |

## Calls made by the author, for Paul to check

Not discussed with Paul; each is a proposal in the document:

- All numbers in table 4.2 other than those marked (Paul), and pack
  prices left blank.
- Free gets 256 MB / 0.5 CPU (5.3).
- No affordability bands in v1 (9.3).
- One apps domain for free and paid, with paid-only custom domains,
  abuse reports and takedown instead of a separate free domain (8.4).
- Storage cap per running thing, not an account pool (5.4).
- Notice rule: after-grace waits for received notice plus 3 days (6).
- Awake-hours enforcement: throttle, close WebSockets, 15-minute
  continuous-awake ceiling (5.2).
- Sponsored limits from measured usage, busiest of the last three months
  for recurring figures (8.7).
- One free account per person checked at creation only (8.1).
- Limits fail open on a box that has never read an account's record (7).
- Outbound port 25 blocked for tenants on plan-governed deployments (8.5).
