Repository navigation
feat(billing): bill cdn and media storage overage, with a 10x abuse ceiling - #449
Conversation
…eiling CDN origin transfer is sold at $0.15 per GB past the plan and media storage at $0.25 per GB-month (average stored over the billing period), the same way AI, API, MCP and form overage are: an opt-in switch per workspace, a Polar metered price plus a meter credit for the plan's allowance, and the usage screen quoting what the meter bills. - catalogue: overage_price back on cdn.bandwidth_gb and media.storage_gb - meters: cdn_origin_gb billable; media storage gets media_storage_gb_months (one event per paying workspace per UTC day, GB / days in its billing period) and a sampler plugin behind NUXT_CDN_STORAGE_METER; the unused byte-hour meter leaves the manifest - limits: with overage on, CDN and storage stop at 10x the plan instead of the int32 soft cap, which read as bytes was 2 GiB and would have shrunk every paid storage limit - locks: the CDN delivery route, the chat media tool and the MCP media facet now drop toggles the subscription cannot bill, like the billing middleware does - alerts, banner and usage panel: the ceiling and how each meter is measured are shown; reaching the ceiling alerts as stopped
…iced - bulk media ingest (bulk-ingest route, Migrate media import) drops toggles the subscription cannot bill before raising the storage cap - a payment account whose priced meters were never reported, or no account at all, counts as locked for cdn and storage overage (`requiresKnownPrice`); the credit and event meters keep the old rule - cdn and storage overage is sellable only while its meter flag is on (NUXT_CDN_ORIGIN_METER / NUXT_CDN_STORAGE_METER, `eventFlag`); flag off is the hard limit of before, whatever is toggled - the plan cards quote the cdn and storage price only once the usage route says this deployment sells it
|
ONAY (reviewer): head 1900a76, test-merge on main de7be8c: test:ci green with --maxWorkers=2 (unit 2247 passed + 1 skipped / 196 files, integration 517 / 54, nuxt 348 / 52), typecheck 0 errors, lint 0 errors (7 existing warnings), check:strings ok. Delta checked: every media cap path now gets lock-filtered settings; an account with no reported priced meters keeps CDN/storage locked (requiresKnownPrice); CDN/storage are sellable only while their meter flag is on (otherwise a hard limit, PATCH 409, no price on plan cards). Mutants, each turns its test red: no lock for unreported meters (1), flag ignored (4), unfiltered settings in bulk ingest (1). Money math, units, the 10× ceiling, overage-off behaviour and the no-migration query were re-checked from the first pass, unchanged. NIT: migration/media.get/post still fall back to the stored settings when no billing context is set. Unreachable in practice (the middleware always sets it), but '{}' would fail closed. |
|
Re-ONAY (ork2) @1f261c3c, after studio main was merged in (t7: --no-commit + a hooked commit, husky lint-staged ran).
|
Founder decision (2026-10-09): CDN bandwidth and media storage overage are charged and billed. This PR makes both work the same way AI, API, MCP and form overage already do: an opt-in switch per workspace, a Polar metered price plus a meter credit for the plan's allowance, and a usage screen that quotes what the meter bills. It also keeps an abuse ceiling.
What was wrong (audit)
overage_priceoffcdn.bandwidth_gb(0.15) andmedia.storage_gb(0.25). Both wereoverageBillable: falseinshared/utils/usage-meters.ts, so polar-sync made no metered price and no meter credit for them, andisOverageSellablekept them hard.server/api/cdn/v1/[projectId]/[...path].get.tswrites bytes per project per UTC day tocdn_usage. Cloudflare edge hits never reach the origin, so they are not counted, which is what egress costs.server/plugins/cdn-origin-meter.tssends onecdn_origin_gbevent per workspace per finished day, but only whenNUXT_CDN_ORIGIN_METER=true(default false).workspaces.media_storage_bytesis a live level. It never reached billing.recordStorageSample(themedia_storage_byte_hoursmeter) had no callers, and a byte-hour meter cannot hold a GB allowance anyway (Polar's meter credit is int32).getEffectiveLimit. With overage on, that returned the int32 soft cap, 2,147,483,647, which as bytes is 2 GiB. That is below every paid plan (Starter 5 GB, Pro 25 GB), so turning storage overage on would have shrunk the cap.cachedWorkspacePlan), the chat media tool (conversation-engine.ts) and the MCP media facet read rawoverage_settings. A staletruewould have raised the cap even on a subscription that has no price for the meter. The billing middleware already drops those toggles; these three paths did not.storage.quota_exceeded) at the plan limit.What this changes
overage_pricegoes back on both rows: $0.15 per GB of CDN origin transfer and $0.25 per GB-month of storage.cdn_origin_gbbecomesoverageBillable: true. It already counts GB, so the plan's GB is the meter credit as is.media_storage_gb_months(sum, 1 unit = 1 GB-month), replacing the unused byte-hour one in the manifest. The old meter is left alone in Polar.server/plugins/media-storage-meter.ts, behindNUXT_CDN_STORAGE_METER. Once per UTC day it sends one event per workspace that has a payment customer: stored GB ÷ the days in that workspace's billing period. A full period therefore sums to the average GB stored, and the included GB is the plan value.DatabaseProvider.listWorkspaceMediaStorageBytes, in both the postgres and supabase providers.OVERAGE_ABUSE_CEILING_RATIO,server/utils/overage.ts). The ceiling is a multiple of the plan, in the caller's unit, so a limit passed in bytes stays in bytes. CDN with overage on stops at the ceiling itself, with no extra 20 % buffer on top (hardStopRatio: 1). With the switch off, the behaviour is the same as today.billableOverageSettings(server/utils/workspace-billing.ts) is now applied in the CDN workspace cache, the chat media tool and the MCP media facet. If billing can't be read, every toggle is treated as off.docs/CDN_EDGE.md,docs/PAYMENT_PROVIDERS.mdand.env.exampleare updated.Label: GENERIC. This is billing infrastructure for two meter classes that every paid workspace has, not a fix for one customer.
Review fixes (1900a76)
media-bulk-ingest.ts(the bulk-ingest route and the Migrate media import) now goes throughbillableOverageSettings. It was the fourth path that readoverage_settingsdirectly.billable_meters(null), or there is no payment account at all, CDN and storage overage counts as locked (requiresKnownPrice). The credit and event meters keep the old behaviour.NUXT_CDN_ORIGIN_METER/NUXT_CDN_STORAGE_METER,eventFlag). With the flag off they behave like today's hard limit, even ifoverage_settingsis true. The plan cards show the CDN and storage price only after the usage route confirms it is sellable.Tests
overage-byte-meters.test.ts(replacesoverage-hard-limits): sellability, the ceiling, byte-unit safety, and unlimited plans.media-storage-meter.test.ts: the sampler. It samples once per day per paying customer, takes the length from the billing or calendar period, skips an account it can't read, and rejects instead of recording "empty" when the listing fails.usage-metering-units.test.ts: the GB and GB-month event values and idempotency keys. A 31-day period of mixed levels sums to the average; Pro at an average of 33.55 GB is charged $2.14.workspace-usage-byte-overage.test.ts: the charge. Pro at 70 GB CDN / 30 GB storage gives 10 × $0.15 + 5 × $0.25 = $2.75. Nothing is quoted while the switch is off or locked.not_in_subscriptionwhen not), and the panel and banner nuxt tests.Gate (HEAD, local)
pnpm test:ci: unit 2247 passed / 1 skipped (196 files), integration 517 passed (54 files), nuxt 348 passed (52 files)pnpm typecheck: 0 errorspnpm lint: 0 errors (7 warnings that were already there, none in changed files)pnpm check:strings: all keys definedSandbox polar-sync dry run (no
--apply,NUXT_POLAR_SERVER=sandbox)The run exits 1 because of two warnings that already existed in sandbox and have nothing to do with this PR: fixed-price drift on "Migrate with Studio Starter/Pro" (7200 vs 9000, 39200 vs 49000 cents).
Known limits
not_in_subscription) until the subscription prices the new meters. See the production steps in the hand-off.Not run against production. No live billing was changed.