Skip to content

feat: billing entitlements — Marketplace plans drive tier limits and the hosted paid gate#66

Merged
BunsDev merged 2 commits into
mainfrom
feat/plan-entitlements
Jul 8, 2026
Merged

feat: billing entitlements — Marketplace plans drive tier limits and the hosted paid gate#66
BunsDev merged 2 commits into
mainfrom
feat/plan-entitlements

Conversation

@BunsDev

@BunsDev BunsDev commented Jul 7, 2026

Copy link
Copy Markdown
Member

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.

  • config: PlanTier (Starter/Team/Dedicated) carrying the docs/pricing.md tier matrix limits; effective_limits precedence (explicit [installations.limits] wins per field — Dedicated contracts, operator overrides); new [billing] require_plan gate.
  • store (schema v6): account_plans + installation_accounts. Marketplace bills accounts; tenancy keys installations — the mapping (kept current by installation events) joins them. Delete-on-uninstall forgets the mapping but keeps the account plan for its other installations.
  • webhook: marketplace_purchase deliveries (purchased / changed / cancelled, free-trial state; pending changes deferred until GitHub sends the effective changed event) upsert the plan idempotently by delivery id and land every transition in the audit trail. Intake resolves plan-tier limits; with require_plan, plan-less installations are recorded ignored:no_plan — the hosted paid gate.
  • worker: claim-time concurrency caps merge plan tiers under explicit TOML caps, re-read each claim so plan changes apply without a restart.

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, the no_plan gate)

Docs: HOSTED.md packaging section, pricing.md enforcement map, config/example.toml [billing] section.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_limits precedence (TOML overrides per field), and [billing] require_plan enforcement.
  • 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_plan when 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 thread crates/worker/src/lib.rs
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)
…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>
@BunsDev BunsDev force-pushed the feat/plan-entitlements branch from 5b8044e to 77a10f8 Compare July 8, 2026 20:18
…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>
@BunsDev BunsDev merged commit 05c3725 into main Jul 8, 2026
1 check passed
@BunsDev BunsDev deleted the feat/plan-entitlements branch July 8, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants