carlos / platform Public
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:
- Pricing page on carloku.com showing Free and Personal, rendered from an approved price book. Can ship before checkout works.
- 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.comis 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, nevercarlos 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 underlockAccount, 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 throughfinishClaimCreateinstead (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
daemoncapability (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 atapisidecars.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
DailyCapstill 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:
- Warning at 80%. Console banner on the account and the affected app, a warning line in
carlosCLI output for commands touching the account, and an email to the account's owners. - 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.
- 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
- 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. - Never-sleep paths refused (5.2).
- Everything that runs counts (5.1).
- 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. - 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. - 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. - 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:
- Grace, 7 days (section 6, including its notice rule). Everything keeps working.
- 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.
- 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/billingstripewithoutinvoices.go(stdlib only, idempotency on every write, webhook signature verification with rotation, portal, customer, catalog checks);internal/entitlement(minus authority lineage). - Adapt:
internal/plansdown to one plan plus packs per currency, no bands or countries, keeping the draft/approved gate andPublicPriceBook;cmd/console/billing.goenv handling with new price keys (<book>/personal/<cur>,<book>/pack/<name>/<cur>); the core ofbillingstore(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 frombillingquantity, aimed at pack items. - Leave behind: everything metered (
usagedurability,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, andbillingretirementunless 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
165472b8queueEntitlementTx(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).
- Ship the plan catalog, limits records and caps dormant (no catalog configured). Nothing changes anywhere.
- 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.
- 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.
- Enable after-grace stages (only for caps whose notice reached owners, section 6).
- 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).