| 1 | # Plans, caps and packs: getting paid plans live on carloku.com |
| 2 | |
| 3 | Status: draft for Paul's review, 2026-10-20. Supersedes the scope (not the |
| 4 | settled policies) of the billing handoff in amadan discussion #321 for the |
| 5 | first paid launch. |
| 6 | |
| 7 | ## 1. Why |
| 8 | |
| 9 | carloku.com charges nobody today. The previous billing effort set out to |
| 10 | bill metered usage (CPU hours, egress, durability writes, storage |
| 11 | byte-months) and to prove every number on an invoice. That grew to 370 |
| 12 | unmerged commits on `feat/carloku-backend-integration-20261003` |
| 13 | (`165472b8`): about 54k lines of product code, 34 files conflicting with |
| 14 | main, and a blocked unit with an unexplained test failure. None of it is |
| 15 | live. |
| 16 | |
| 17 | Paul's direction (2026-10-20): get plans live first, worry about overages |
| 18 | later. The shape that makes that possible: |
| 19 | |
| 20 | - **Charge for counts the console already knows, never for measured |
| 21 | usage.** An app count, a pack quantity, a plan. Nothing on an invoice |
| 22 | needs proving because nothing on an invoice was measured. |
| 23 | - **Measure only to enforce.** Sizes, sends, bytes and awake hours are |
| 24 | still measured, but only to warn and to stop. A measurement that is an |
| 25 | hour late means someone hits a wall an hour late. Nobody is |
| 26 | overcharged. |
| 27 | |
| 28 | The moment we are building for: someone has tried Carloku, deployed a free |
| 29 | app, and wants another. They hit the free limit, and they pay to lift it, |
| 30 | in the console, there and then. |
| 31 | |
| 32 | ## 2. Scope |
| 33 | |
| 34 | Two deliveries, in this order: |
| 35 | |
| 36 | 1. **Pricing page** on carloku.com showing Free and Personal, rendered from |
| 37 | an approved price book. Can ship before checkout works. |
| 38 | 2. **Plans, caps, packs and checkout** in the platform: a free plan, one |
| 39 | paid annual plan (Personal) bought through Stripe Checkout, packs sold |
| 40 | as quantities on the same subscription, caps with warnings and grace |
| 41 | periods, and the abuse guards in section 8. |
| 42 | |
| 43 | ### Goals |
| 44 | |
| 45 | - A new account gets the free plan and its caps, with no card. |
| 46 | - A free account that hits a cap can buy Personal (and packs) in the |
| 47 | console and is lifted immediately on payment. |
| 48 | - Every cap warns at 80%, allows a grace period, then refuses only the |
| 49 | capped action. Apps keep serving. Data is never deleted by a cap, |
| 50 | except what a lapsed or reduced paid account no longer pays for, and |
| 51 | only after grace, a 90-day disabled period and the Trash (9.2). |
| 52 | - Existing finite Carloku accounts stay sponsored: they act as paid, at |
| 53 | no charge (settled policy, #321). |
| 54 | - Plans are a general platform feature. A deployment with no plan catalog |
| 55 | configured (Tito, self-hosted boxes, dev unless switched on) behaves |
| 56 | exactly as today: no caps, no billing. |
| 57 | |
| 58 | ### Non-goals (later, not never) |
| 59 | |
| 60 | - Metered usage billing and overages of any kind. |
| 61 | - Team plans priced per member (Community, Business). |
| 62 | - Affordability bands (see 9.3). |
| 63 | - Seats as a sellable pack. |
| 64 | - An always-on option on any plan. |
| 65 | - Billing storage requests, replication writes or egress by volume. |
| 66 | |
| 67 | ### Anti-goals (would make this a failure even if it works) |
| 68 | |
| 69 | - A surprise bill. Nothing is charged that the customer did not choose. |
| 70 | - Losing a customer's data without warning, or while they are still |
| 71 | paying for it. |
| 72 | - A free tier that can be farmed without limit, which makes every cap |
| 73 | meaningless. |
| 74 | - Carloku-specific code paths in the platform. carloku.com is a |
| 75 | deployment that configures plans; it gets no special case. |
| 76 | - Re-importing the old branch wholesale, or its review process. |
| 77 | |
| 78 | ## 3. Decisions already made |
| 79 | |
| 80 | By Paul in this conversation (2026-10-20), not to be reopened by review: |
| 81 | |
| 82 | - Scope is (1) the pricing page and (2) flat plans plus packs. Overages |
| 83 | wait. |
| 84 | - Free: **3 apps**. Free apps **sleep after 1 to 2 minutes idle**. |
| 85 | - Paid apps **stay awake up to 15 minutes** after the last request. |
| 86 | - **All apps hibernate.** That is what makes the economics work. No plan |
| 87 | offers always-on. |
| 88 | - **Packs** raise caps: extra apps, storage, email, bandwidth. They are |
| 89 | bought, not metered. |
| 90 | - **Every cap has a grace period with fair notice** before anything is cut |
| 91 | off. |
| 92 | - **Bandwidth** follows the same cap pattern. |
| 93 | - **After a lapse, paid-only features get a grace period, are then |
| 94 | disabled, and are eventually deleted.** Agreed details: 7 days grace, |
| 95 | 90 days disabled with reminders, then the Trash's 30 days (9.2). |
| 96 | - Abuse guards: account farming, things that never sleep, and counting |
| 97 | everything that runs are in v1. Phishing defences are in v1 if free and |
| 98 | paid apps share a domain. Outbound abuse starts with an audit. Hidden |
| 99 | costs start as monitoring. |
| 100 | |
| 101 | Settled earlier and still binding (#321 and the 2026-10-01 rulings): |
| 102 | |
| 103 | - Existing finite accounts are sponsored; sponsorship stays with the |
| 104 | account through ownership transfer; once limits apply, exhaustion pauses |
| 105 | safely and data is kept at Carloku's expense. No automatic charging. |
| 106 | - `console.carloku.com` is the paid deployment. Tito and other |
| 107 | self-managed deployments stay billing OFF. No hostname-triggered |
| 108 | activation. |
| 109 | - Three currencies, lower case: usd, eur, gbp. Default by billing country |
| 110 | (EU member states eur, GB gbp, everything else usd), with a visible |
| 111 | override. An explicit currency we do not sell is refused, never |
| 112 | defaulted. Nothing is converted. |
| 113 | - A live console refuses a draft price book; approval is a new book |
| 114 | version, never an edit. |
| 115 | - Stripe keys are restricted `rk_` keys in the console's EnvironmentFile, |
| 116 | never the bucket, never `carlos secrets set`. |
| 117 | |
| 118 | ## 4. Plans and limits |
| 119 | |
| 120 | ### 4.1 The catalog |
| 121 | |
| 122 | A deployment opts in by configuring a **plan catalog**: a versioned file |
| 123 | (shape left to the builder; JSON beside `internal/plans` is the obvious |
| 124 | home) that names each plan's caps and each pack's increment. No catalog |
| 125 | configured means no caps and no billing. |
| 126 | |
| 127 | An account's **effective limits** are its plan's caps plus, for each pack, |
| 128 | quantity times increment, never above the pack's ceiling. The console |
| 129 | computes them. Every enforcement point reads effective limits, never a |
| 130 | plan name. |
| 131 | |
| 132 | ### 4.2 Numbers |
| 133 | |
| 134 | Paul's numbers are marked **(Paul)**. Everything else is a proposal for |
| 135 | Paul to approve or change before the book is approved; the builder treats |
| 136 | them as configuration, not constants. |
| 137 | |
| 138 | | Limit | Free | Personal | Pack adds | Hard ceiling | |
| 139 | | --- | --- | --- | --- | --- | |
| 140 | | Running things (apps + instances, 5.1) | 3 **(Paul)** | 25 | +5 | 200 without an operator | |
| 141 | | Idle window before sleep | 90 s (1 to 2 min **(Paul)**) | 15 min **(Paul)** | none | 15 min | |
| 142 | | 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 | |
| 143 | | Awake hours per month (account) | 100 | 1,500 | none in v1 | n/a | |
| 144 | | Database + files on box (per running thing, 5.4) | 1 GB | 10 GB | +10 GB to every running thing | 2x cap, and box safety | |
| 145 | | Sidecar workers per app | 1 | 5 | none in v1 | 5 | |
| 146 | | Object storage (per account) | operator-approved only, 1 GB if approved | 25 GB self-serve | +50 GB | 2x cap | |
| 147 | | Email per day (account) | 50 | 1,000 | +1,000 | 10,000 without an operator | |
| 148 | | Bandwidth per month (account) | 10 GB | 200 GB | +200 GB | 2x cap, then throttled | |
| 149 | | Free accounts per person | 1 | n/a | n/a | n/a | |
| 150 | | Custom domains | none | allowed | n/a | n/a | |
| 151 | | Point-in-time database history | latest copy only | 7 days | none | n/a | |
| 152 | |
| 153 | Prices: Personal at the draft book's annual bases (usd 28, eur 24, gbp 22). |
| 154 | Pack prices are Paul's to set; the book needs one price per pack per |
| 155 | currency. |
| 156 | |
| 157 | ## 5. The caps |
| 158 | |
| 159 | Every cap has the same three stages (section 6). This section says, per |
| 160 | cap, what is counted, where it is enforced, and what "refused" means. |
| 161 | |
| 162 | ### 5.1 Running things (the app cap) |
| 163 | |
| 164 | **Counted:** apps, temporary apps, and customer instances (including |
| 165 | seeded and imported). Each counts as one. Sidecar workers do not count |
| 166 | against the running-things cap; they have their own per-app cap (table |
| 167 | 4.2: 1 on free, 5 on Personal), enforced at `handleAPISidecarCreate` |
| 168 | (`apisidecars.go:216`), and they obey the plan's idle window (5.2). Fleets |
| 169 | are customer-owned boxes, not apps; free accounts cannot create fleets. |
| 170 | |
| 171 | **Grace and creation (codex 1.2).** Refusing to create something new is |
| 172 | the cap itself, not a cut-off, so it has no grace: the warning at 80% |
| 173 | is the notice. Grace protects what already runs. It applies whenever a |
| 174 | lowered limit would take something away from something running. |
| 175 | |
| 176 | **Enforced at:** |
| 177 | - `claimAppWith` (`internal/console/server.go:4052`), the single claim |
| 178 | check both create doors and temporary apps already use. The temporary |
| 179 | app cap (`apitemporary.go:24,66,167`) is the template: checked under |
| 180 | `lockAccount`, refused with 409 and `"code":"cap"`. |
| 181 | - `restoreApp` (`restore.go:55`) and the claims-mode restore |
| 182 | (`claim_restore.go:120`), which do not pass through the claim. |
| 183 | - Instance creation. `provision.CreateRecord` |
| 184 | (`internal/provision/provision.go:882`) is **not** a common choke point |
| 185 | (codex 1.10): claims-mode creation goes through `finishClaimCreate` |
| 186 | instead (`apiinstances.go:1658`). The check belongs in the console |
| 187 | handlers before either write: `handleAPIInstanceCreate` |
| 188 | (`apiinstances.go:987`, which the seeded and imported variants pass |
| 189 | through) and the first-route attach (`domainattach.go:237`), under the |
| 190 | same account lock as the app claim so the two counts cannot race. |
| 191 | - Direct mode skips the claim (`server.go:~4030`). The builder must either |
| 192 | count those apps when their rows are filled from the bucket and apply |
| 193 | the over-cap stages, or refuse direct-mode creation for plan-governed |
| 194 | accounts. Leaving it open is not an option. |
| 195 | |
| 196 | **Over the cap:** creation is refused. Nothing that already runs is |
| 197 | touched. An account can be over the cap only by losing a plan or a pack |
| 198 | (lapse, downgrade, reduction at renewal). Then: grace (section 6), and |
| 199 | after grace the **newest** running things beyond the cap are disabled |
| 200 | (stop serving, show a clear page), and, if still over, deleted after the |
| 201 | retention period (9.2). This is the one case where a cap leads to |
| 202 | deletion. |
| 203 | |
| 204 | ### 5.2 Idle window, and things that never sleep |
| 205 | |
| 206 | **Idle window.** Today `cfg.Idle` is box-wide (`CARLOS_HIBERNATE_IDLE`, |
| 207 | default 15 min, `internal/activator/activator.go:101,123`), and only |
| 208 | sidecar workers carry a per-route window (`rt.IdleSeconds`, |
| 209 | `activator.go:3114`, used at `:4646`). The plan's idle window reaches the |
| 210 | box as a per-route `IdleSeconds` set by the provision reconcile pass |
| 211 | (`internal/provision/reconcile.go:441` for instances, |
| 212 | `sidecar_reconcile.go:144` for sidecars) from the account's effective |
| 213 | limits, and the activator honours a per-route window for every entry, not |
| 214 | only workers. A route's window is the smaller of its own and the plan's. |
| 215 | |
| 216 | **Never sleeping is refused for plan-governed accounts:** |
| 217 | - the `daemon` capability (`internal/caps/caps.go:106`), refused at |
| 218 | release promote/ship time with a message naming the plan, not just at |
| 219 | converge; |
| 220 | - `SidecarManifest.Hibernate = false` (`provision/sidecar.go:56`, |
| 221 | member-settable at `apisidecars.go:614`); |
| 222 | - any member-reachable path that yields an always-on route. Address-proxy |
| 223 | routes default to always-on (`provision.go:444`); the builder confirms |
| 224 | whether a member can create one and closes it if so. |
| 225 | |
| 226 | Operators keep `carlos route --hibernate=false` for their own needs. |
| 227 | |
| 228 | **Schedules.** `MinEvery` is one minute (`internal/schedule/spec.go:21`) |
| 229 | and a due tick holds the host awake (`activator.go:~4655`). Free accounts |
| 230 | get a minimum schedule interval of 15 minutes, validated where `ValidEvery` |
| 231 | runs. Paid keeps one minute. |
| 232 | |
| 233 | **Awake hours.** A monthly awake-hours cap per account backstops keep-alive |
| 234 | pingers, open WebSockets and mining. The source is the per-account |
| 235 | `instance_hours` figure the hourly economics pass already writes to |
| 236 | `usage/accounts/<acct>/<month>.json` (`economicspass.go:378-397,835`). It |
| 237 | is approximate and that is acceptable: it only gates. |
| 238 | |
| 239 | A shorter idle window cannot stop an app that is never idle: a steady |
| 240 | pinger, a long stream or an open WebSocket keeps `inflight` above zero |
| 241 | (`activator.go:4650`) (codex 1.1). So after grace the account is |
| 242 | **throttled** through the same per-account throttle state as bandwidth |
| 243 | (5.7), its open WebSockets are closed and new upgrades refused, and each |
| 244 | app gets a continuous-awake ceiling (the activator's existing run ceiling, |
| 245 | `MaxRunSeconds`, applied to instances): after 15 minutes awake the app is |
| 246 | put to sleep and wakes on its next request. Apps still wake and serve, |
| 247 | slowly, until the month rolls or the account upgrades. |
| 248 | |
| 249 | ### 5.3 Memory and CPU |
| 250 | |
| 251 | Add a third tier beside `TierDefault` and `TierChildren` |
| 252 | (`internal/cgroup/cgroup.go:92`): 256 MB, CPU quota 50%. `memTier` |
| 253 | (`internal/host/memtier.go`) chooses it from the route's account limits. |
| 254 | The tier lists that hard-refuse unknown tiers need the new entry: |
| 255 | `strict_linux.go:216`, `strict_gen_linux.go:162`, `gen.go:65`, and |
| 256 | unit-backed apps' numbers in `caps/render.go`. |
| 257 | |
| 258 | A new app gets its plan's tier at once. An app that is already running |
| 259 | when its account's limit drops (lapse, downgrade) keeps its old tier |
| 260 | through the 7-day grace and moves to the lower tier at its next start |
| 261 | after grace (codex 1.2). Lowering memory under a running process would |
| 262 | kill it, which is the cut-off without notice the grace rule forbids. What |
| 263 | is new is the **explanation**: when an app is OOM-killed under the free tier, the |
| 264 | console's app page and `carlos` CLI status say so in plain words and |
| 265 | point at the upgrade. A free app that cannot run in 256 MB should fail |
| 266 | loudly and once, not restart-loop silently. |
| 267 | |
| 268 | ### 5.4 Database and files on the box |
| 269 | |
| 270 | Today nothing stops one app filling a box's disk (single gp3 root volume, |
| 271 | `infra/modules/region-host/main.tf:493`; data at `/data/<host>.db` and |
| 272 | `/data/instances/<id>/`, `provision.go:2912`, `bootstrap.go:75`). This is |
| 273 | a live risk whether or not anyone pays, so it is in v1. |
| 274 | |
| 275 | **Enforcement:** a per-app quota on the app's data directory, enforced by |
| 276 | the kernel, so writes fail at the hard limit with no polling and no race. |
| 277 | Filesystem quotas fit the policy exactly: a soft limit with a grace timer |
| 278 | and a hard limit. The builder chooses the mechanism (ext4 project quotas, |
| 279 | an XFS data volume with project quotas, or another), and the choice may |
| 280 | mean new boxes get a data volume built for it. |
| 281 | |
| 282 | **The unit is the running thing (codex 1.5).** Each app or instance has |
| 283 | its own data directory on exactly one box (`provision.go:2960`), so the |
| 284 | quota is per data directory, and a storage pack raises the cap of every |
| 285 | running thing on the account. This sells more total disk than an account |
| 286 | pool would, but every running thing is already counted and paid for, and |
| 287 | it needs no allocation across boxes. |
| 288 | |
| 289 | **Boxes without quota support (codex 1.6).** Existing boxes keep working |
| 290 | without a quota until they are replaced. On those boxes the disk cap is |
| 291 | **warn-only**: warnings and reminders, no after-grace stage, and never a |
| 292 | stop or a refused wake for disk. The box-level disk alert and placement's |
| 293 | 10% free-disk floor (`boxchoice/policy.go:22`) remain the protection |
| 294 | there. The disk cap's after-grace stage switches on per box, only where |
| 295 | the quota exists. Replacing flagship boxes with quota-capable ones is a |
| 296 | rollout prerequisite for that stage only, not for launch. |
| 297 | |
| 298 | **Counting for warnings:** a periodic size check per data directory (the |
| 299 | quota's own usage report where available) feeds the 80% warning and the |
| 300 | grace clock. |
| 301 | |
| 302 | **After grace:** writes fail (SQLite sees a full disk). Reads keep |
| 303 | working. The app page says why. |
| 304 | |
| 305 | ### 5.5 Object storage |
| 306 | |
| 307 | Free accounts keep today's rule: an operator approves storage |
| 308 | (`internal/console/apistore_entitlement.go`, `objstore/entitlement.go`). |
| 309 | Paid accounts get it self-serve up to their cap. |
| 310 | |
| 311 | **Counting:** `sizeProvisionedStore` (`economicspass.go:1023`) already |
| 312 | lists each store bucket's bytes daily into `usage/storage/<date>.json`. |
| 313 | Daily is enough. |
| 314 | |
| 315 | **After grace:** a `Deny s3:PutObject` statement in the store's policy |
| 316 | (`storePolicyJSON`, `internal/objstore/aws.go:180`; `EnsureUser` rewrites |
| 317 | the whole inline policy). Reads and deletes keep working, so the customer |
| 318 | can clean up. The MinIO adapter needs the equivalent (section 10). |
| 319 | |
| 320 | Downloads straight from a store do not pass the edge, so they are outside |
| 321 | the bandwidth cap. Storage request counts are monitored, not capped, in |
| 322 | v1 (8.6). |
| 323 | |
| 324 | ### 5.6 Email |
| 325 | |
| 326 | Half the mechanism exists: daily caps by tier |
| 327 | (`internal/console/mailcounts.go:67`), counted from SES events, enforced by |
| 328 | pausing a sender's key (`mail/walk.go:1120`) until the UTC day rolls. But |
| 329 | it is **per sender, not per account** (codex 1.11): `enforceMailLimits` |
| 330 | reads one sender's counts against that sender's persisted grant |
| 331 | (`mailcounts.go:253`), and `mailBillingTier` (`apimail.go:245`, always |
| 332 | `"free"`, #226) is only consulted when a grant is created |
| 333 | (`apimail.go:1297`). v1 needs: |
| 334 | |
| 335 | - an account-wide daily count, summed over the account's senders, checked |
| 336 | against the account's effective email limit; at the limit every sender |
| 337 | of the account is paused; |
| 338 | - the per-sender cap kept as it is (an operator's `DailyCap` still works); |
| 339 | - existing grants re-derived when the account's limit changes (plan, |
| 340 | pack, lapse), not only at grant creation. |
| 341 | |
| 342 | Email's cap resets daily, so a week-long grace does not fit. Its grace is |
| 343 | **within the day**: warn at 80%, allow up to 2x the cap, pause at 2x. The |
| 344 | 10,000 a day hard ceiling is not raised by packs; only an operator raises |
| 345 | it, because sending reputation is shared by every tenant. |
| 346 | |
| 347 | ### 5.7 Bandwidth |
| 348 | |
| 349 | **Counting:** the edge already counts response bytes per route |
| 350 | (`internal/edge/usagemeter.go:36`), flushed every 5 minutes into |
| 351 | `usage/edge/<box>/days/<date>.json` and summed per account per month as |
| 352 | `egress_gb` by the economics pass (`economicspass.go:409-427`). Approximate |
| 353 | and late by up to an hour: acceptable for a gate. |
| 354 | |
| 355 | **Gap:** WebSocket traffic after the upgrade is not counted |
| 356 | (`edge.go:1704`). v1 accepts that and relies on the awake-hours cap. |
| 357 | |
| 358 | **After grace:** throttle, do not refuse. The account's routes are served |
| 359 | at a reduced rate until the month rolls or the account upgrades. This |
| 360 | needs a per-account throttle state that every edge can see; it travels in |
| 361 | the account's limits record (section 7). Grace for a monthly cap is "the |
| 362 | rest of the month, up to 2x the cap". |
| 363 | |
| 364 | ## 6. Warnings, grace and notice |
| 365 | |
| 366 | One engine, used by every cap except memory: |
| 367 | |
| 368 | 1. **Warning at 80%.** Console banner on the account and the affected |
| 369 | app, a warning line in `carlos` CLI output for commands touching the |
| 370 | account, and an email to the account's owners. |
| 371 | 2. **Over the cap, in grace.** Everything keeps working. A daily reminder |
| 372 | says what is over, by how much, when grace ends, and the three ways |
| 373 | out: buy a pack, upgrade, or clean up. |
| 374 | 3. **After grace.** Only the capped action is refused (per cap, above). |
| 375 | The app keeps serving. Data is untouched. |
| 376 | |
| 377 | Grace periods: 7 days for counts and sizes (apps, disk, storage); |
| 378 | "the rest of the day up to 2x" for email; "the rest of the month up to 2x" |
| 379 | for bandwidth and awake hours. Every grace has a hard ceiling behind it |
| 380 | (table 4.2): grace never means unlimited growth. |
| 381 | |
| 382 | State lives in the console (one row per account per cap: first over, |
| 383 | grace ends, last notified, notice received), so a reminder is sent once |
| 384 | per period and a console restart does not reset a clock. |
| 385 | |
| 386 | **What counts as notice (codex 1.4).** Notice is received when any of |
| 387 | these is recorded for an owner of the account, after the cap went over: |
| 388 | the warning email was accepted for delivery; the owner loaded a console |
| 389 | page that showed the banner; or the owner ran a CLI command that printed |
| 390 | the warning. **An after-grace stage starts only when grace has ended and |
| 391 | notice was received at least 3 days earlier.** An unattended account |
| 392 | whose owners were never reached stays in grace. Two things do not wait |
| 393 | for notice: hard ceilings (they protect the box and every other tenant, |
| 394 | not our economics) and refusing to create something new. |
| 395 | |
| 396 | **Dependency:** the console cannot email account owners today; giving it |
| 397 | an SES sending grant is an open security ruling (the same one holding |
| 398 | console invite delivery, see memory `console-invite-delivery`). Until it |
| 399 | is settled, notice is only the console banner and CLI warning. |
| 400 | |
| 401 | ## 7. How limits reach the boxes |
| 402 | |
| 403 | The console is the single writer of an account's effective limits. It |
| 404 | writes them to one record in the bucket per account; boxes read it the |
| 405 | same way they already read `accounts/<sqid>/account.json` |
| 406 | (`domains/reconcile.go:573`, exposed to the account's own boxes by |
| 407 | `edge/storedoor.go:376`). |
| 408 | |
| 409 | The integration branch's `internal/entitlement` record |
| 410 | (`entitlements/accounts/<sqid>.json`, with |
| 411 | `Runtime{IdleSeconds, MaxApps, MaxMembers, MemoryTier, ...}`) is the shape |
| 412 | to start from. Harvest the record; leave its writer-lineage ("authority") |
| 413 | machinery behind unless the builder finds a second writer, which v1 should |
| 414 | not have. |
| 415 | |
| 416 | Boxes use the record for: idle window, memory tier, throttle state, |
| 417 | suspended routes, and the disk quota size. The console uses its own |
| 418 | database for everything it enforces itself (creation caps, email, |
| 419 | storage policy, custom domains, schedules). |
| 420 | |
| 421 | A box that cannot read the record keeps the last one it read. A box that |
| 422 | has never read one for an account applies **no** caps (fail open), and |
| 423 | reports it. Caps protect economics, not security; failing closed would |
| 424 | take customer apps down on a bucket hiccup. |
| 425 | |
| 426 | ## 8. Abuse guards |
| 427 | |
| 428 | 1. **One free account per person.** `mintAccount` (`server.go:2226`) is the |
| 429 | path for every console account creation (auto-mint on first sign-in, |
| 430 | `/accounts/new`, the CLI). A person may own one free account. More |
| 431 | accounts are created as paid (checkout first) or by an operator. |
| 432 | `carlos accounts create --owner` (operator, bucket-direct) is exempt. |
| 433 | Address farming is expected; detecting farms is later, not v1. |
| 434 | The rule applies at creation only (codex 1.8). A person who owns |
| 435 | several paid accounts that later lapse keeps them all, each on free |
| 436 | limits: each was paid for, and the alternative means deleting or |
| 437 | charging. Ownership transfers are not checked against it either. |
| 438 | 2. **Never-sleep paths refused** (5.2). |
| 439 | 3. **Everything that runs counts** (5.1). |
| 440 | 4. **Phishing and malware.** v1 keeps one apps domain for free and paid |
| 441 | (a separate free domain would rename apps on every upgrade; see 9.2). |
| 442 | Because they share it, v1 ships: custom domains for paid accounts only |
| 443 | (gate in `attachDomainAs`, `domainattach.go:116`, above the uniqueness |
| 444 | checks per that file's ordering rule); an abuse-report address on the |
| 445 | site and in each app's footer page; and an operator takedown command |
| 446 | that suspends an app and its account's other apps with a reason. |
| 447 | 5. **Outbound abuse.** Units already deny private ranges and metadata |
| 448 | (`IPAddressDeny`), but the public internet is open and nothing blocks |
| 449 | outbound port 25. v1: block outbound SMTP (25) from tenant units on |
| 450 | plan-governed deployments, and write down what else is open. Further |
| 451 | egress policy is later. |
| 452 | 6. **Hidden costs, monitored.** Per-app wakes per day (a 90-second window |
| 453 | can turn steady light traffic into a sleep/wake cycle every few |
| 454 | minutes; wakes reuse the warm local database, `backend_exec.go:784`, |
| 455 | but still take a lease and a checkpoint), replication request rates, |
| 456 | store request counts, and ship frequency (the staging disk is never |
| 457 | reaped, amadan #122). Reported per account on the operator economics |
| 458 | view. No caps in v1. |
| 459 | 7. **Sponsored accounts have limits too.** "Cover current usage" means a |
| 460 | sponsored account's effective limits are, per limit, the larger of |
| 461 | Personal's cap and its measured usage, recorded once when plans switch |
| 462 | on (codex 1.9): |
| 463 | - counts and sizes (running things, disk, storage): as measured on |
| 464 | switch-on day; |
| 465 | - recurring figures (email, bandwidth, awake hours): the highest full |
| 466 | month in the three months before switch-on, from the existing |
| 467 | monthly usage statements (`usage/accounts/<acct>/<month>.json`); |
| 468 | email from the per-day counts, highest day. |
| 469 | A recorded sponsored limit may exceed a table 4.2 hard ceiling; it is |
| 470 | then an explicit operator-approved figure on that account, listed for |
| 471 | Paul before switch-on. Increases after that are reviewed separately |
| 472 | (settled policy). Nothing is invented: an account with no statement |
| 473 | history gets Personal's caps. |
| 474 | |
| 475 | ## 9. Checkout and packs |
| 476 | |
| 477 | ### 9.1 What gets sold |
| 478 | |
| 479 | One subscription per account, annual, in the account's currency: |
| 480 | |
| 481 | - one item: Personal, quantity 1; |
| 482 | - zero or more items: one per pack type, quantity = number of packs. |
| 483 | |
| 484 | Adding a pack mid-year updates the item's quantity with Stripe proration. |
| 485 | Reducing a pack takes effect at renewal. Upgrading from free is Stripe |
| 486 | Checkout in subscription mode. Managing payment method and invoices is |
| 487 | the Stripe customer portal, locked to those functions. The portal does not |
| 488 | change plans or quantities; the console does. |
| 489 | |
| 490 | ### 9.2 Lapse and downgrade |
| 491 | |
| 492 | A failed renewal follows Stripe's retry schedule. While Stripe retries, the |
| 493 | account keeps its paid limits. When the subscription ends, the account |
| 494 | drops to free limits and section 6 applies to every cap it is now over, |
| 495 | with its normal grace. |
| 496 | |
| 497 | **What the account no longer pays for is disabled, then deleted (Paul, |
| 498 | 2026-10-20, codex 1.7).** This covers everything a lapse, downgrade or |
| 499 | pack reduction leaves beyond free (or the remaining) limits: running |
| 500 | things beyond the cap and their data, sidecars beyond the cap, attached |
| 501 | custom domains, and fleets. The steps: |
| 502 | |
| 503 | 1. **Grace, 7 days** (section 6, including its notice rule). Everything |
| 504 | keeps working. |
| 505 | 2. **Disabled.** Stops serving; a custom domain stops routing; a fleet |
| 506 | takes no new work. Configuration and data are kept. Paying again (or |
| 507 | buying the pack back) re-enables it at once. |
| 508 | 3. **Deleted, 90 days after being disabled**, with reminders 30, 7 and 1 |
| 509 | days before. Deletion goes through the existing Trash |
| 510 | (`release.TrashCooloff`, 30 days, `internal/release/trash.go:33`), so |
| 511 | it can still be restored for a month. Data is gone for good about four |
| 512 | months after the lapse at the earliest. |
| 513 | |
| 514 | Which running things are kept is the account's choice where it makes |
| 515 | one before grace ends (pick which three stay); otherwise the newest go |
| 516 | first. Schedules faster than the free minimum are not deleted: after |
| 517 | grace they run at the free minimum interval. |
| 518 | |
| 519 | **Sponsored accounts are never on this path.** They keep the settled |
| 520 | policy: paused at exhaustion, data kept at Carloku's expense. |
| 521 | |
| 522 | ### 9.3 Bands |
| 523 | |
| 524 | The original model scaled price by an affordability band (four bands per |
| 525 | plan). v1 sells at one price per currency (band A). Bands multiply the |
| 526 | catalog by four and thread through checkout metadata, intents and |
| 527 | records; they can be added later as more prices without changing the |
| 528 | model. (My call; Paul to confirm.) |
| 529 | |
| 530 | ### 9.4 Code to harvest from `165472b8` |
| 531 | |
| 532 | Not a merge. Copy onto current main, adapt, and review as new code. |
| 533 | |
| 534 | - **Copy:** `internal/billingstripe` without `invoices.go` (stdlib only, |
| 535 | idempotency on every write, webhook signature verification with |
| 536 | rotation, portal, customer, catalog checks); `internal/entitlement` |
| 537 | (minus authority lineage). |
| 538 | - **Adapt:** `internal/plans` down to one plan plus packs per currency, no |
| 539 | bands or countries, keeping the draft/approved gate and |
| 540 | `PublicPriceBook`; `cmd/console/billing.go` env handling with new price |
| 541 | keys (`<book>/personal/<cur>`, `<book>/pack/<name>/<cur>`); the core of |
| 542 | `billingstore` (accounts, checkout intents, book assignments, event |
| 543 | dedupe, rejected events, reviews), `billingservice`, `billinghandlers`, |
| 544 | `billingpage`, `billingoperator` (the house flag), `entitlementstore` |
| 545 | (fill limits from plan plus packs); the quantity update and fence from |
| 546 | `billingquantity`, aimed at pack items. |
| 547 | - **Leave behind:** everything metered (`usage` durability, |
| 548 | `resourcebudget`, `awsbilling`, `authority`, invoices, calculation, tax |
| 549 | split, statements, overage preferences, `month.go`, `lines.go`), bands |
| 550 | and countries, the legacy-EUR path, sponsored cohort enrollment and |
| 551 | observation, and `billingretirement` unless the builder shows v1 needs |
| 552 | it. |
| 553 | - **Sponsorship needs a new path (codex 1.12).** The harvested house flag |
| 554 | does not cover it on its own: at `165472b8` `queueEntitlementTx` |
| 555 | (`internal/console/entitlementstore.go:219`) publishes nothing for an |
| 556 | account with no subscription, and sponsored accounts never check out. |
| 557 | v1 needs a subscription-independent plan assignment (an operator sets |
| 558 | an account's plan, with its recorded sponsored limits from 8.7) that |
| 559 | writes the same limits record. Self-hosted deployments without a billing |
| 560 | provider use the same path (section 10). |
| 561 | - **Main rework:** the old code assumes one subscription item everywhere |
| 562 | (`billingservice.go:939`, `billingquantity.go:1015`). Packs mean several |
| 563 | items. Reconciliation, intent matching and price recognition must handle |
| 564 | a set of items, each recognised against the book. |
| 565 | |
| 566 | Tests are rewritten for v1, not copied. |
| 567 | |
| 568 | ### 9.5 Open decisions from the old branch that still apply |
| 569 | |
| 570 | From `DECISIONS-FOR-PAUL.md` on the integration branch: 1 to 5 (money on |
| 571 | a refused purchase, what "settled" requires, Stripe contradicting itself, |
| 572 | an unbound checkout, what the buyer sees during review), 6 to 8 recast |
| 573 | for pack reductions, 9 (who is free on switch-on day: answered here, |
| 574 | existing accounts are sponsored, new ones are free), and 15 |
| 575 | (`incomplete_expired`). The builder proposes an answer for each in the |
| 576 | plan; Paul rules. |
| 577 | |
| 578 | ### 9.6 Stripe live setup |
| 579 | |
| 580 | Following the old `ENABLEMENT.md` procedure, reshaped: products (Personal, |
| 581 | one per pack type), prices (one per product per currency, yearly, |
| 582 | licensed, tax-exclusive), the 100% house coupon, a locked portal |
| 583 | configuration, the webhook (`checkout.session.completed`, |
| 584 | `customer.subscription.{created,updated,deleted}`, `invoice.paid`, |
| 585 | `invoice.payment_failed`), the restricted key, and the console variables. |
| 586 | IE and OSS tax registrations already exist. Every live step is Paul's to |
| 587 | approve; a test-mode rehearsal on dev comes first. |
| 588 | |
| 589 | ## 10. Provider boundary |
| 590 | |
| 591 | - Caps, grace, notices, plan catalog and limits records are platform core: |
| 592 | no AWS or Stripe dependency. A self-hosted deployment can configure |
| 593 | plans with no billing provider at all (an operator assigns plans). |
| 594 | - Stripe is a billing adapter behind the console's billing service. |
| 595 | - Store write blocking is an objstore adapter operation: AWS writes a |
| 596 | Deny statement; MinIO writes its own policy equivalent. |
| 597 | - Disk quotas are a host (Linux) mechanism; non-Linux builds get a stub |
| 598 | (cross-compile gate). |
| 599 | - Outbound port blocking is a host mechanism, Linux only. |
| 600 | |
| 601 | ## 11. Pricing page (delivery 1) |
| 602 | |
| 603 | The page lives in the carloku.com site (deployed from `bes/website`, see |
| 604 | memory `platform-hostnames-209`; earlier drafts in the |
| 605 | `carloku-paid-pricing` worktree, rendered from |
| 606 | `copy-review/pricing/strings.json` and `public-pricebook.json`). It shows |
| 607 | Free and Personal from the approved book's public export, the caps table |
| 608 | in human terms, and the packs. The upgrade button goes to the console; |
| 609 | until checkout is live it goes to sign-up. |
| 610 | |
| 611 | Gates: an approved price book (a new version, not an edit of the draft); |
| 612 | copy review of every string (the 2026-09-22 approval is lost and does not |
| 613 | count); the site refuses to publish from a draft book. |
| 614 | |
| 615 | ## 12. Rollout |
| 616 | |
| 617 | All live steps need Paul. Dev before flagship (roll order rule). |
| 618 | |
| 619 | 1. Ship the plan catalog, limits records and caps **dormant** (no catalog |
| 620 | configured). Nothing changes anywhere. |
| 621 | 2. Dev: configure a catalog; mark every existing dev account sponsored; |
| 622 | create fresh free accounts and exercise every cap through grace and |
| 623 | after grace; Stripe test mode end to end, including a lapse. |
| 624 | 3. Flagship: mark every existing account sponsored (house) with limits |
| 625 | recorded from current usage. Configure the catalog. New accounts are |
| 626 | free. Watch a week of warnings with after-grace stages disabled. |
| 627 | 4. Enable after-grace stages (only for caps whose notice reached owners, |
| 628 | section 6). |
| 629 | 5. Stripe live provisioning (9.6), then switch checkout on. Publish the |
| 630 | pricing page's upgrade button. |
| 631 | |
| 632 | ## 13. Left to the builder |
| 633 | |
| 634 | - Catalog and limits record file shapes. |
| 635 | - The disk quota mechanism and the data-volume change it needs. |
| 636 | - How throttling is applied at the edge. |
| 637 | - Delivery slicing, beyond the order: limits and records, then caps, then |
| 638 | notices and grace, then checkout, then the page. |
| 639 | - Copy for every refusal and warning (through copy review). |
| 640 | |
| 641 | ## 14. Review record |
| 642 | |
| 643 | Round 1, Astra builder check at `5ff0e014`, findings committed unedited as |
| 644 | `CODEX-FINDINGS-PLANS-CAPS-1.md`. Numbering here: questions 1 to 9 are |
| 645 | codex 1.1 to 1.9; the three citation errors, in order, are codex 1.10 to |
| 646 | 1.12. All twelve are accepted: |
| 647 | |
| 648 | | Finding | Where answered | |
| 649 | | --- | --- | |
| 650 | | 1.1 awake-hours cap cannot stop a never-idle app | 5.2: throttle, close WebSockets, continuous-awake ceiling | |
| 651 | | 1.2 grace vs creation refusal and memory reductions | 5.1, 5.3 | |
| 652 | | 1.3 sidecars unbounded on paid | table 4.2, 5.1 | |
| 653 | | 1.4 what counts as notice | section 6 | |
| 654 | | 1.5 storage unit | 5.4: per running thing | |
| 655 | | 1.6 boxes without quotas | 5.4: warn-only there | |
| 656 | | 1.7 paid-only features after downgrade | 9.2, Paul's ruling: grace, disable, delete after 90 days via the Trash | |
| 657 | | 1.8 one free account per person after lapse | 8.1: creation only | |
| 658 | | 1.9 sponsored "current usage" measurement | 8.7 | |
| 659 | | 1.10 `CreateRecord` is not the instance choke point | 5.1 | |
| 660 | | 1.11 email caps are per sender | 5.6 | |
| 661 | | 1.12 house flag publishes no limits without a subscription | 9.4 | |
| 662 | |
| 663 | ## Calls made by the author, for Paul to check |
| 664 | |
| 665 | Not discussed with Paul; each is a proposal in the document: |
| 666 | |
| 667 | - All numbers in table 4.2 other than those marked (Paul), and pack |
| 668 | prices left blank. |
| 669 | - Free gets 256 MB / 0.5 CPU (5.3). |
| 670 | - No affordability bands in v1 (9.3). |
| 671 | - One apps domain for free and paid, with paid-only custom domains, |
| 672 | abuse reports and takedown instead of a separate free domain (8.4). |
| 673 | - Storage cap per running thing, not an account pool (5.4). |
| 674 | - Notice rule: after-grace waits for received notice plus 3 days (6). |
| 675 | - Awake-hours enforcement: throttle, close WebSockets, 15-minute |
| 676 | continuous-awake ceiling (5.2). |
| 677 | - Sponsored limits from measured usage, busiest of the last three months |
| 678 | for recurring figures (8.7). |
| 679 | - One free account per person checked at creation only (8.1). |
| 680 | - Limits fail open on a box that has never read an account's record (7). |
| 681 | - Outbound port 25 blocked for tenants on plan-governed deployments (8.5). |
| 682 | |