Payments, subscriptions and metering
Read references/PAYMENTS-AND-MONETIZATION.md before applying any of this.
That file is the standard; everything below it is a summary to help you decide
whether this skill applies and to check your work afterwards.
What this standard covers
- The model in one paragraph
- Merchant of record vs payment gateway
- Mock-first
- Webhooks
- Subscription lifecycle
- Quota and metering
- Entitlement enforcement is layered
- Tenant mode
- Introducing paid tiers to an existing user base
Failure modes
| Symptom | Cause |
|---|---|
| Every production webhook rejected | Signature header name assumed, not confirmed; or webhook secret unset (correct behaviour — set the secret) |
| Webhooks accepted in production without signatures | The mock-mode fallback was made symmetric; restore reject-in-production |
| Subscription grants applied twice | No idempotency on provider event id; redelivery is normal, not exceptional |
| Cancellations never take effect | Lifecycle events landing in the mapper's default branch; only payment events were handled |
| Paying user loses days after early renewal | Extension computed from payment date instead of max(expiry, payment date) |
| Renewal-day usage missing from the new period | Quota period reset from the extended expiry base, not the payment date |
| Webhook dropped: "order not found" | Provider webhook beat your checkout write; synthesize from payload and backfill |
| Users blocked when the quota service restarts | Thin client fails closed; only 429 may block |
| Workspace counter wrong forever | State-counted quota incremented on events instead of re-synced from the database |
| Free users briefly see paid features | Client gate is optimistic while loading — by design; the API policy is the boundary |
Checklist
Per payment integration:
- Mock mode: synthetic checkout + authenticated simulate endpoint running the real webhook path; simulate returns 403 outside mock mode
- Raw-body signature verification, constant-time compare; unsigned → accept in mock, reject in production
- Idempotency keyed on provider event id; amount and currency validated server-side
- Provider events mapped to an internal vocabulary; default branch logged; lifecycle events explicitly handled
- Webhook-before-order tolerated (cache → DB → synthesize; upsert backfills)
- Extension math never shortens; metering period resets from payment date
- Cancel-at-period-end + resume; downgrade locks excess resources with a reason, never deletes
- Provider vocabulary and DTOs confined to the boundary (P11)
Per metering setup:
- Central quota authority; thin fail-open client in the shared kernel (429 blocks, everything else allows, ~5 s timeout)
- Consume-before vs consume-after decided and written down
-
-1= unlimited sentinel; tier profiles as pure functions; lazy period reset - State-counted quotas re-synced at read; event-counted quotas append-only; race-safe row creation
- Usage event log kept alongside counters
-
TenantModeimplemented at token issuance only; frontend hides pricing via a flag - Paid-tier rollout has an announce + grace period plan
Generated from docs/guides/PAYMENTS-AND-MONETIZATION.md by scripts/build-marketplace.mjs. Do not edit this file: change the source document, or its entry in catalog/marketplace.catalog.json, and re-run the generator.