feat: billing entitlements — Marketplace plans drive tier limits and the hosted paid gate#66
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces a billing entitlement layer that ties GitHub Marketplace plans to enforced per-installation limits (task caps and concurrency) and adds an optional hosted “paid gate” requiring an entitled plan unless the installation is explicitly configured.
Changes:
- Add config-level plan tier model (
PlanTier),effective_limitsprecedence (TOML overrides per field), and[billing] require_planenforcement. - Extend the durable store schema (v6) with per-account plan records and installation→account mappings used to resolve entitlements.
- Update webhook routing and worker claim logic to apply plan-derived limits, record/audit Marketplace transitions, and enforce
ignored:no_planwhen gated.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| HOSTED.md | Documents how Marketplace purchases feed enforcement and how require_plan behaves as a paid gate. |
| docs/pricing.md | Updates the enforcement map to include purchase→entitlement mechanics and require_plan. |
| crates/worker/src/lib.rs | Merges plan-derived concurrency caps with TOML caps at claim time (and re-reads to pick up plan changes). |
| crates/webhook/src/routes.rs | Handles marketplace_purchase, maintains installation→account mapping, resolves entitlements per task, and enforces require_plan gate. |
| crates/webhook/src/events.rs | Extends webhook payload types to include Marketplace purchase data and installation account identity. |
| crates/store/src/lib.rs | Adds schema v6 tables and store APIs for account plans and installation account mapping; supports entitlement resolution. |
| crates/config/src/lib.rs | Adds billing config, plan-tier classification, and limit precedence helper (effective_limits). |
| config/example.toml | Adds documented [billing] configuration example and behavior notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+737
to
+738
| let tier: coven_github_config::PlanTier = | ||
| p.tier.parse().unwrap_or(coven_github_config::PlanTier::Unknown); |
Comment on lines
+52
to
+56
| match store.entitled_installation_tiers().await { | ||
| Ok(tiers) => { | ||
| for (installation, tier) in tiers { | ||
| if let std::collections::hash_map::Entry::Vacant(slot) = | ||
| caps.entry(installation) |
e2e795d to
5b8044e
Compare
…the hosted paid gate Turn purchased plans into enforced entitlements, the monetization layer on top of the shipped limit knobs (docs/pricing.md): - config: PlanTier (starter/team/dedicated) with the pricing-matrix limits, effective_limits precedence (explicit TOML wins per field), and a [billing] require_plan gate for hosted deployments. - store (schema v6): account_plans + installation_accounts — Marketplace bills accounts, tenancy keys installations; the mapping joins them. Uninstall forgets the mapping but keeps the account's plan. - webhook: marketplace_purchase deliveries (purchased/changed/cancelled, trial state, pending changes deferred) upsert the plan idempotently and land in the audit trail; installation events keep the account mapping current; intake resolves plan-tier limits and, with require_plan, records plan-less installations as ignored:no_plan. - worker: claim-time concurrency caps merge plan tiers under explicit TOML caps, re-read each claim so plan changes apply without restart. Unknown plan names fail safe to Starter limits so a listing rename never grants unlimited service or locks a paying customer out. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Val Alexander <bunsthedev@gmail.com>
5b8044e to
77a10f8
Compare
…h delivery dedup Review finding: the handler recorded the delivery id in one transaction and applied the plan in another. A storage failure between the two marked the delivery seen while dropping the transition — a redelivery would dedupe and never repair it, leaving a cancelled account entitled forever or a paying customer locked out under require_plan. record_delivery_with_plan now lands the dedup row and the plan upsert in one Immediate transaction (mirroring record_delivery_sync's task handling), so 'delivery seen' and 'plan applied' cannot diverge. Stale replays still cannot clobber newer state. Audit remains non-fatal: observability, not entitlement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Val Alexander <bunsthedev@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The entitlement layer between billing and the shipped enforcement knobs — the first piece of the monetization surface on top of the completed hosted-V1 spine.
PlanTier(Starter/Team/Dedicated) carrying the docs/pricing.md tier matrix limits;effective_limitsprecedence (explicit[installations.limits]wins per field — Dedicated contracts, operator overrides); new[billing] require_plangate.account_plans+installation_accounts. Marketplace bills accounts; tenancy keys installations — the mapping (kept current byinstallationevents) joins them. Delete-on-uninstall forgets the mapping but keeps the account plan for its other installations.marketplace_purchasedeliveries (purchased / changed / cancelled, free-trial state; pending changes deferred until GitHub sends the effectivechangedevent) upsert the plan idempotently by delivery id and land every transition in the audit trail. Intake resolves plan-tier limits; withrequire_plan, plan-less installations are recordedignored:no_plan— the hosted paid gate.Unknown plan names fail safe to Starter limits, so a Marketplace listing rename never grants unlimited service or locks a paying customer out.
Why
Executes the pricing principle "every limit is enforced, not promised" one level up: every plan is enforced, not provisioned by hand. Purchase → entitlement needs no TOML edit, which is what makes hosted self-serve. The billing source is pluggable — a Stripe webhook can feed the same tables.
Testing
cargo check --all-targets✅cargo clippy --all-targets -- -D warnings✅cargo test --all✅ (244 passed, 0 failed; 16 new tests: tier mapping/precedence, store resolution/uninstall, purchase/cancel/trial/pending/duplicate flows, theno_plangate)Docs: HOSTED.md packaging section, pricing.md enforcement map,
config/example.toml[billing]section.