{"protocol":"clank-doc/1","frameworkVersion":"0.12.0","slug":"hosted-plans-and-billing","title":"Hosted plans and billing","description":"Clank can turn installation limits into a public plan catalog without moving authorization or quota enforcement into a payment provider. Every account sees its current plan, exact monthly price, billing state, and effective entitlements at ","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/hosted-plans-and-billing","source":"docs/hosted-plans-and-billing.md","headings":["Entitlement order","Inspect an account","Configure a plan catalog","Connect Stripe","Checkout and webhook lifecycle","Provider neutral API","Operational checklist"],"tableOfContents":[{"id":"entitlement-order","title":"Entitlement order","level":2},{"id":"inspect-an-account","title":"Inspect an account","level":2},{"id":"configure-a-plan-catalog","title":"Configure a plan catalog","level":2},{"id":"connect-stripe","title":"Connect Stripe","level":2},{"id":"checkout-and-webhook-lifecycle","title":"Checkout and webhook lifecycle","level":2},{"id":"provider-neutral-api","title":"Provider-neutral API","level":2},{"id":"operational-checklist","title":"Operational checklist","level":2}],"markdown":"# Hosted plans and billing\n\nClank can turn installation limits into a public plan catalog without moving authorization or\nquota enforcement into a payment provider. Every account sees its current plan, exact monthly\nprice, billing state, and effective entitlements at `/billing` and through `clank billing`.\nPlatform administrators can grant a plan directly or add narrower quota overrides from the\nexisting account limits editor.\n\nBilling is optional. A self-hosted installation with no billing configuration starts exactly as\nbefore, exposes no billing route or navigation, and continues to use installation and operator\nlimits. A catalog can also run without a payment provider, which is useful for private,\ninvite-only, sponsored, or manually administered installations.\n\n## Entitlement order\n\nClank resolves capacity at the point where a resource or request is admitted:\n\n1. current installation defaults;\n2. the account's durable snapshot of the plan's explicitly declared entitlements;\n3. explicit account overrides set by a platform administrator; and\n4. for workspace-scoped limits, explicit workspace overrides.\n\nAn `active`, `trialing`, or operator-granted `manual` plan contributes its snapshot. A `past_due`\nsubscription keeps its snapshot only through the configured grace interval. `canceled`, `unpaid`,\n`paused`, `incomplete`, and expired states fall back to the default plan. Invalid persisted\nentitlements also fail closed to the default.\n\nLowering or losing an entitlement never deletes a project, release, backup, domain, or workspace.\nExisting resources remain visible and operable, but creation is denied until usage is back within\nthe effective limit. This makes cancellation and payment failures reversible instead of turning\nthem into destructive storage operations.\n\nThe available quota keys are:\n\n| Key | Scope |\n| --- | --- |\n| `organizationsPerAccount` | account |\n| `projectsPerAccount` | account |\n| `projectsPerOrganization` | account or workspace |\n| `domainsPerProject` | account or workspace |\n| `releasesPerProject` | account or workspace |\n| `releaseStorageBytesPerProject` | account or workspace |\n| `backupsPerProject` | account or workspace |\n| `requestsPerMonthPerOrganization` | account or workspace |\n| `transferBytesPerMonthPerOrganization` | account or workspace |\n| `requestsPerMinutePerProject` | account or workspace |\n\nSee [Usage and limits](usage-and-limits.md) for exact metering boundaries. A plan price does not\nturn known transfer into an invoice: Clank reports the configured price and enforces capacity,\nwhile the payment provider owns payment collection, tax behavior, receipts, and invoices.\n\n## Inspect an account\n\nOpen **Billing** in the control plane, or use the read-only CLI command:\n\n```sh\nclank billing\nclank billing --json\n```\n\nThe normal command shows the current plan, state, period or grace deadline, the effective limits\nafter operator overrides, and the browser management URL. `--json` returns the unmodified\n`clank-billing/1` API document for an agent or script.\n\nAn authenticated account or account-wide token can also read:\n\n```http\nGET /api/billing\n```\n\nProject-scoped tokens cannot read account billing. Checkout and portal creation deliberately\nrequire an interactive browser session plus Clank's CSRF token; a bearer token cannot initiate\neither operation.\n\n## Configure a plan catalog\n\n`clank-platform` accepts the catalog through one of:\n\n- `CLANK_BILLING_PLANS_JSON`, for a small inline JSON document; or\n- `CLANK_BILLING_PLANS_FILE`, for a regular, non-symbolic-link file.\n\nThe two settings are mutually exclusive and the input is limited to 256 KiB. Unknown fields,\nunknown quotas, duplicate IDs, invalid prices, and out-of-range values stop startup. One through\ntwenty plans are allowed. The default plan must exist and have a zero monthly amount.\n\n```json\n{\n  \"defaultPlanId\": \"free\",\n  \"pastDueGraceMs\": 604800000,\n  \"plans\": [\n    {\n      \"id\": \"free\",\n      \"name\": \"Free\",\n      \"description\": \"A small place to try Clank.\",\n      \"monthlyPrice\": { \"currency\": \"usd\", \"amount\": 0 },\n      \"quotas\": {\n        \"projectsPerAccount\": 2,\n        \"backupsPerProject\": 3\n      }\n    },\n    {\n      \"id\": \"pro\",\n      \"name\": \"Pro\",\n      \"description\": \"Capacity for active side projects.\",\n      \"monthlyPrice\": { \"currency\": \"usd\", \"amount\": 1500 },\n      \"quotas\": {\n        \"projectsPerAccount\": 20,\n        \"projectsPerOrganization\": 20,\n        \"domainsPerProject\": 10,\n        \"backupsPerProject\": 30\n      },\n      \"featured\": true,\n      \"stripePriceId\": \"price_replace_with_your_recurring_price\"\n    }\n  ]\n}\n```\n\n`monthlyPrice.amount` uses the currency's smallest unit: `1500` is USD 15.00. At most one plan may\nbe featured. `pastDueGraceMs` defaults to seven days and must be from one hour through ninety\ndays. A plan only needs to declare values that differ from the installation defaults; the public\ncatalog shows the fully resolved values.\n\nThe `stripePriceId` field is provider configuration and is stripped from the public catalog. Do\nnot place API keys, webhook secrets, customer IDs, or subscription IDs in this file.\n\nFor a catalog-only installation, set no Stripe variables. Operators can open **Control plane →\nAccounts → Limits…** and choose an **Operator-granted plan**. Grants and revocations are durable\nand audited. A provider-managed subscription cannot be overwritten with a manual plan; use its\nbilling portal or an explicit quota override.\n\n## Connect Stripe\n\nCreate one recurring Stripe Price for every non-free catalog plan, put its `price_…` ID in the\nmatching catalog entry, and set both secrets:\n\n```sh\nCLANK_STRIPE_SECRET_KEY=sk_live_replace_me\nCLANK_STRIPE_WEBHOOK_SECRET=whsec_replace_me\n```\n\nFor test mode, use matching `sk_test_…`, test Price IDs, and a test-mode webhook. Live and test\nevents cannot cross. Configure the endpoint:\n\n```text\nhttps://your-control-plane.example/api/billing/webhook\n```\n\nSubscribe it to:\n\n- `checkout.session.completed`\n- `customer.subscription.created`\n- `customer.subscription.updated`\n- `customer.subscription.deleted`\n\nEnable and configure Stripe's customer portal before exposing the **Manage billing** action.\nClank expects one configured subscription item with quantity one; portal plan changes must select\none of the catalog's mapped Prices.\n\nClank normally lets Stripe use the API version configured on the account. To pin a version that\nyou have already tested in Stripe Workbench, set:\n\n```sh\nCLANK_STRIPE_API_VERSION=2026-02-25.clover\n```\n\nThis is optional and intentionally has no hard-coded default. Changing a pinned provider API\nversion is an operator-controlled compatibility change.\n\nRestart the control plane after changing catalog or secret environment variables. Startup logs\nreport only whether billing is disabled, catalog-only, or provider-backed and the plan count;\nthey never print secret or provider resource values.\n\n## Checkout and webhook lifecycle\n\nThe browser asks Clank to create checkout for a configured paid plan. Clank creates a durable,\nexpiring attempt first and sends its random ID as both Stripe idempotency and metadata. The\ncheckout return URL is fixed to the configured control-plane origin. The Stripe adapter only\naccepts hosted redirects on `checkout.stripe.com`. Before the first checkout for each plan in a\nrunning process, the adapter retrieves the configured Stripe Price and verifies that it is active,\nin the matching live/test mode, uses the catalog currency and exact unit amount, and is a\nlicensed, per-unit monthly recurring Price. A mismatch fails closed before Clank returns a\ncheckout URL. Successful verification is cached in memory because Stripe Price amount and\ncurrency are immutable.\n\nWebhook processing then:\n\n1. reads the exact raw request body with a 512 KiB limit;\n2. verifies Stripe's HMAC-SHA256 signature and five-minute timestamp window;\n3. verifies live/test mode and the event's bounded shape;\n4. binds account, plan, customer, subscription, checkout attempt, and session identifiers;\n5. records the event ID and digest transactionally for replay/conflict detection; and\n6. applies only a newer subscription event to the entitlement snapshot.\n\nSubscription events may arrive before checkout completion. Clank handles that ordering, rejects\nconflicting reuse of an event ID, and treats exact replays as successful duplicates. Provider\nerrors returned to users are generic; raw provider bodies and identifiers are not written to\nactivity metadata.\n\n## Provider-neutral API\n\nProgrammatic installations can supply any adapter implementing `BillingProvider`:\n\n```ts\nimport { openPlatform } from \"@clank.run/framework/platform\";\nimport type { BillingProvider } from \"@clank.run/framework/billing\";\n\ndeclare const provider: BillingProvider;\n\nconst platform = await openPlatform({\n  dataDirectory: \"./data\",\n  publicUrl: \"https://platform.example\",\n  billing: {\n    defaultPlanId: \"free\",\n    plans: [\n      {\n        id: \"free\",\n        name: \"Free\",\n        description: \"Start small.\",\n        monthlyPrice: { currency: \"usd\", amount: 0 },\n        quotas: { projectsPerAccount: 1 }\n      },\n      {\n        id: \"pro\",\n        name: \"Pro\",\n        description: \"More capacity.\",\n        monthlyPrice: { currency: \"usd\", amount: 1500 },\n        quotas: { projectsPerAccount: 20 }\n      }\n    ],\n    provider\n  }\n});\n```\n\nThe adapter owns hosted checkout, hosted account management, and signed-event normalization.\nClank owns account authorization, durable attempts, event ordering, entitlement snapshots, quota\nprecedence, and audit. The provider's `planIds` must exactly equal the paid catalog plan IDs so a\npartial or stale mapping cannot start.\n\nThe included `createStripeBillingProvider()` adapter pins Stripe's API origin, disables redirects\nand credentials, bounds response bodies and timeouts, validates all provider IDs and hosted URLs,\nand never exposes its API key.\n\n## Operational checklist\n\nBefore enabling paid checkout:\n\n1. exercise checkout, portal, upgrade, downgrade, cancellation, payment failure, recovery, and\n   duplicate webhook delivery in provider test mode;\n2. verify every paid Price is recurring, active, unique, and mapped to exactly one plan;\n3. confirm the public control-plane URL is HTTPS and the webhook points to the same deployment;\n4. configure provider tax, receipt, refund, dispute, legal, and support behavior for your business;\n5. back up the Clank control database and test restore;\n6. alert on webhook HTTP failures and `BILLING_PROVIDER_UNAVAILABLE`;\n7. review operator grants and quota overrides in the activity feed; and\n8. confirm a downgrade blocks new capacity without deleting existing resources.\n\nBilling records live in the same control-plane SQLite database as accounts and quotas, so the\nexisting encrypted backup and restore procedures cover them. Provider payment data remains with\nthe provider; Clank stores only opaque customer/subscription references and the minimum state\nneeded to authorize capacity. Account deletion cascades its billing state and checkout attempts.\nAt startup, unreferenced checkout attempts older than thirty days are removed; the attempt that\nbinds a current provider subscription remains. Provider event IDs and digests contain no account\nor payment payload and are retained for seven years for replay/conflict detection, then pruned at\nstartup. Choose an independent provider-side financial-record retention policy for your legal and\naccounting requirements.\n"}