markdown · 22492 bytes Raw Blame History

Billing and paid organizations

shithub's first paid surface is organization billing. This document records the product and engineering contract that the PAYMENTS sprint series implements.

The current implementation already has the important shape for paid organizations: orgs.plan is an enum with free, team, and enterprise; organizations own repositories; organization members and teams exist; branch protection and PR review gates exist; Actions has schema for org/repo secrets, variables, and artifacts. Billing must turn that substrate into a fair hosted service without taxing public/open-source collaboration.

Product contract

As of 2026-05-12, GitHub's public pricing page presents Free at $0, Team at $4/user/month, and Enterprise starting at $21/user/month. shithub follows the same mental model but removes Copilot/AI promises from the paid-org offering.

Initial decisions:

  • Free organizations remain self-serve.
  • Team is $4 per active organization member per month.
  • Active organization members, including owners, count as paid seats.
  • Team has no launch trial.
  • Enterprise is a visible contact-sales stub, not self-serve.
  • Stripe Billing is the first payment processor.
  • PayPal, manual invoices, SAML, SCIM, LDAP, enterprise account hierarchy, and contracts are deferred.
  • Self-serve organization creation presents /organizations/plan as the canonical plan selector. Choosing Team creates the organization and immediately redirects the owner to hosted Stripe Checkout.

The fairness rule is explicit: public/open-source collaboration should stay generous. Paid gates focus on private collaboration, hosted cost, advanced organization controls, and support expectations.

Pricing copy rules

Pricing and onboarding pages must describe only features shithub can actually deliver on the hosted service. Before changing pricing copy, refresh the official GitHub pricing source and the Stripe Billing docs because both are time-sensitive inputs.

Rules for paid-org copy:

  • Do not mention Copilot, AI agents, AI code review, or AI quotas.
  • Do not promise SAML, SCIM, LDAP, managed users, audit exports, data residency, compliance attestations, contracts, or custom support until the matching implementation sprint ships.
  • Do not advertise Packages, Pages, Wikis, Projects, Actions minutes, or storage quotas until those surfaces have enforcement and usage accounting.
  • Use upgrade language for unavailable Team features instead of hiding existing data. Downgrades preserve configuration and make gated settings read-only where possible.
  • Keep public/open-source collaboration generous in both copy and enforcement. Avoid copy that makes public repositories feel like a second-class Free tier.
  • Enterprise is a contact-sales stub in v1. It should collect interest without promising contractual features.

Entitlement matrix

Capability Free Team Enterprise stub
Public org repositories Included Included Contact sales
Basic private org repositories Included Included Contact sales
Org members and invitations Included Billed by active member Contact sales
Effective private org collaborators Up to 3 Unlimited while active/in grace Contact sales
Visible teams Included Included Contact sales
Secret teams Upgrade Included Contact sales
Basic branch protection Included Included Contact sales
Advanced private-repo branch protection Upgrade Included Contact sales
Required reviewers on private org repos Upgrade Included Contact sales
CODEOWNERS review Deferred Deferred Deferred
Org-level Actions secrets Upgrade Included Contact sales
Org-level Actions variables Upgrade Included Contact sales
Actions minutes Low quota once metered Higher quota once metered Contact sales
Actions artifacts/storage Low quota once metered Higher quota once metered Contact sales
Packages storage Deferred until Packages is active Deferred until Packages is active Deferred
Pages/Wikis/Projects Do not promise until shipped Do not promise until shipped Deferred
Audit log export Deferred Deferred Later Enterprise feature
SAML/SCIM/managed users Deferred Deferred Later Enterprise feature
Data residency/compliance Deferred Deferred Later Enterprise feature
Billing support Basic instance support Billing support after runbook exists Contact sales

Pro v1 user-tier matrix (PRO07)

Pro is the user-tier paid plan (single seat, $4/month). PRO01 ratified the v1 feature set; PRO07 wires the enforcement matrix. CODEOWNERS is a registered placeholder with a no-op enforce path until the parser ships.

Capability Free Pro
Public/private personal repos Included Included
Required reviewers on private personal repos Upgrade Included
Multi-reviewer (>1 approvals) on private personal repos Upgrade Included
Advanced branch protection on private personal repos Upgrade Included
CODEOWNERS review Deferred Deferred
Profile pins 6 100

Multi-reviewer is not a separate feature constant — the numeric threshold lives in the deny payload of FeatureRequiredReviewers.

Per-feature enforcement flags

PRO05 plumbed user-kind report-only logging through every gating site. PRO07 lights up the gates one feature at a time via billing.enforce.* in the operator's config. Defaults are all false (report-only). Each flag is a one-way deploy that operators can roll back without code changes.

Config key Gate site Default
billing.enforce.user_required_reviewers internal/web/handlers/repo/settings_branches.go false
billing.enforce.user_advanced_branch_protection internal/web/handlers/repo/settings_branches.go false
billing.enforce.user_profile_pins_beyond_free internal/web/handlers/profile/pins.go false

Rollout discipline:

  1. Deploy with all flags false. Run the report-only telemetry query for 7 days. Confirm zero unexpected user-kind would-denies.
  2. Flip one feature flag in staging. Soak 7 days.
  3. Flip the same feature flag in production.
  4. Repeat per feature.

PRO07's pitfall doc explicitly forbids enforcing a feature without an unenforce path. New gating sites land with their own flag; do not share flags across features.

Downgrade preservation

users.plan = 'free' after cancellation grandfather's existing gated state — required-reviewer rules, profile pins above 6, advanced flags on existing rules. The gate refuses to create new gated state on Free, but never deletes prior configuration. This is the same contract as the org-tier downgrade.

Current capability audit

Already present and safe to gate:

  • Organizations with plan and billing_email.
  • Organization members, owner role, and invitations.
  • Teams, including privacy='secret'.
  • Branch protection rules and required review counts.
  • PR review and reviewer-request substrate.
  • Org/repo Actions secrets and variables schema.

Present but missing enforcement or metering:

  • Storage quota type exists, but quota persistence and enforcement are incomplete.
  • Actions minutes, artifacts, and object usage need accounting before paid limits can be promised.
  • Packages storage cannot be sold until the Packages sprint is active and quota enforcement exists.

Deferred:

  • SAML, SCIM, LDAP, enterprise account hierarchy, audit-log export, data residency, compliance promises, and custom support SLAs.
  • Copilot/AI features are intentionally outside shithub's paid-org product.

Billing architecture

Stripe is the payment source of truth. shithub is the entitlement source of truth.

The billing implementation should add a local billing domain that stores only Stripe IDs and payment summaries, never card data. Webhooks update local subscription state after signature verification. Policy and request handlers read local billing/entitlement state and must not call Stripe in hot paths.

Required local concepts:

  • Stripe customer per billable organization.
  • Subscription state per organization.
  • Subscription item ID for seat quantity sync.
  • Immutable webhook receipts with unique provider event IDs.
  • Invoice/payment summaries for UI.
  • Seat snapshots for auditability.
  • Billing grace/lock state derived from processed subscription events.

PAYMENTS SP02 adds these as local database tables:

  • org_billing_states stores the organization billing projection used by entitlement checks.
  • billing_seat_snapshots records active and billable seat counts over time.
  • billing_invoices stores invoice/payment summaries for billing UI.
  • billing_webhook_events stores immutable provider event receipts for idempotent webhook processing.

New organizations receive a Free billing state from a database trigger, and the migration backfills existing organizations as Free. Subscription snapshot writes also keep orgs.plan synchronized as the human-facing summary.

PAYMENTS SP03 adds the first Stripe operator contract:

  • billing.enabled=false keeps paid-org flows disabled while retaining the local billing tables.
  • billing.stripe.secret_key, billing.stripe.webhook_secret, and billing.stripe.team_price_id are required before Stripe routes are mounted.
  • Checkout success, Checkout cancel, and Billing Portal return URLs may be overridden explicitly; otherwise the web layer derives absolute organization URLs from auth.base_url.
  • billing.grace_period controls how long failed-payment states may remain unlocked before paid entitlements are cut off.
  • When billing routes are mounted, /settings/organizations links owner-managed organizations to their billing page's plan comparison. When billing is disabled, that action stays visibly unavailable instead of linking to an unmounted route.

The operator enablement flow is documented in runbooks/stripe-billing.md.

PAYMENTS SP04 adds the self-serve onboarding flow:

  • /organizations/plan is the canonical plan picker.
  • Free setup creates the organization locally without Stripe.
  • Team setup creates the organization, creates or reuses the Stripe customer, counts billable seats, and redirects directly to hosted Stripe Checkout.
  • Checkout success and cancel returns render shithub pages. Success tells the owner that activation waits for webhook processing; cancel keeps the organization on Free and offers a retry path.

PAYMENTS SP05 adds the local entitlement boundary. Product code must ask internal/entitlements for feature decisions instead of inspecting orgs.plan directly. The package derives access from org_billing_states, understands billing-good-standing states, and returns upgrade metadata for user-facing handlers.

PAYMENTS SP06 wires the first Team gates:

  • Secret teams require Team to create. Existing secret teams remain visible to authorized viewers after downgrade; owners can remove members and repository grants, but adding members or granting more repository access is blocked until Team billing is active again.
  • Required reviewers and advanced status-check branch protection are Team-only for private organization repositories. Public organization repositories keep those safety controls available on Free.
  • Downgraded private organization repositories may delete protection rules or submit a rule update that clears the gated review/check settings.
  • Org-level Actions secrets and variables require Team for create or update in both HTML settings and REST API routes. Delete stays available so owners can clean up gated configuration after downgrade.
  • Org-level Actions secrets and variables API routes require organization owner or site-admin access before entitlement checks.

PAYMENTS SP07 completes the first self-serve billing settings surface:

  • GET /organizations/{org}/settings/billing is owner-only and is linked from organization settings when Stripe Billing is configured.
  • The page shows current local plan state, subscription status, payment source summary, recent Stripe-synced invoice snapshots, and actionable banners for past-due, grace-period, canceled, scheduled-cancel, and billing-action-needed states.
  • Seat accounting is shown as three separate values: current active members, billable seats from the latest local billing state, and pending invitations. Pending invitations are explicitly not billed until accepted.
  • Team organizations manage payment method, invoices, cancellation, and downgrade through Stripe Billing Portal. shithub never collects card data directly and downgrades continue to preserve paid configuration as read-only data.
  • Normal organization owners do not see raw Stripe customer, subscription, or subscription-item IDs. Site admins see a debug panel with those IDs and the latest locally recorded webhook receipt state.

PAYMENTS SP06a adds the first private-collaboration limit:

  • Free organizations may have up to 3 unique humans with effective access to at least one private organization repository.
  • Team organizations with active, trialing, or in-grace subscriptions have unlimited private collaborators.
  • The effective private-collaborator set counts org owners, direct collaborators on private org repos, and team members who inherit a private repo grant through direct team membership or one-level parent team inheritance. Plain org members do not count unless they gain private repo access through one of those paths.
  • Public repository collaboration never counts toward the limit.
  • Downgrades preserve existing access even when the org is already over the Free limit, but writes that add a new effective private collaborator are blocked until the org upgrades or removes access.
  • Creating/importing a private org repository and changing an org repo from public to private are blocked on Free when the resulting private collaborator set would exceed the limit.
  • Cleanup writes remain available: removing org members, team members, direct collaborators, team repo grants, and gated configuration must not require Team.

Entitlement architecture

Paid feature checks must live behind a central entitlement package, not as scattered orgs.plan checks in handlers.

make lint-org-plan enforces this boundary. Schema/sqlc plumbing may store and scan the plan value, but product behavior should ask the entitlement package whether a feature key is available.

The package entrypoint is entitlements.ForOrg(ctx, deps, orgID), which loads the local org_billing_states projection and returns a request-scoped entitlement set. Callers then use CanUse(feature) for feature decisions and Limit(name) for paid limit metadata. The legacy CheckOrgFeature helper is a thin wrapper for handlers that need only one feature. These calls are deterministic and never call Stripe.

Expected feature keys:

  • org.secret_teams
  • org.advanced_branch_protection
  • org.required_reviewers
  • org.actions_org_secrets
  • org.actions_org_variables
  • org.private_collaboration_limit
  • org.storage_quota
  • org.actions_minutes_quota

Authorization and entitlement are separate gates. A user must have both the policy permission and the paid entitlement for gated writes. Denials must preserve existing policy.Maybe404 behavior where existence leaks matter.

Entitlement outcomes are:

  • Free organizations receive upgrade_required for Team features.
  • Team organizations with active or trialing subscriptions receive the feature.
  • Team organizations in past_due remain usable only while their local grace window has not expired.
  • Team organizations with incomplete, unpaid, paused, canceled, or expired-grace billing receive billing_action_needed.
  • Enterprise remains a contact-sales stub and receives enterprise_contact_sales until a later Enterprise sprint defines a real feature set.

Handler upgrade helpers map paid-feature denials to HTTP 402 metadata and a billing-settings path, but handlers must still perform normal authorization first and preserve 404 masking for private resources.

Downgrade behavior

Downgrades must preserve customer data. Moving from Team to Free should not delete teams, secrets, variables, branch rules, or review settings. Existing gated resources become read-only where possible. Users can remove gated configuration, but cannot create or expand it until the organization upgrades again.

Open questions for implementation

  • Exact Free and Team quota numbers for Actions and storage. These must come from real host-cost estimates before SP08.

Source references

  • GitHub pricing: https://github.com/pricing
  • GitHub plans docs: https://docs.github.com/en/get-started/learning-about-github/githubs-plans
  • Stripe Billing: https://docs.stripe.com/billing
  • Stripe pricing models: https://docs.stripe.com/products-prices/pricing-models

PRO08 GA audit closure

Audit dates: 2026-05-13 (run + remediation in a single sprint).

Scope: PRO04 Stripe adapter + PRO05 entitlements Principal + PRO06 user-tier billing settings + PRO07 Pro v1 feature gates.

Methodology: four parallel security/correctness audit agents ran read-only inspections against the PRO07 tip plus the deployed host (shithub.sh). Findings were triaged per-bug; HIGH and MEDIUM severity gaps were fixed inline on the payments-pro/08-pro-ga-audit branch with locking tests. LOW-severity items were either fixed or documented per the user directive "we don't want to take any chances with payments."

Findings + remediation status

# Severity Finding Fix Test
A1 MED guardPriceKindMatch bypassable on empty Items refuse empty-items when prices configured TestBillingWebhookGuardRefusesEmptyItemsWhenPricesConfigured
A2 HIGH (subject_kind, subject_id) never written to receipts SetWebhookEventSubjectForPrincipal after every resolve; ListFailedWebhookEvents operator query TestSetWebhookEventSubjectForPrincipalRecordsAuditTrail, TestListFailedWebhookEventsReturnsErroredAndStuckEntries
A3 MED Concurrent dup-replay TOCTOU race session-scoped advisory lock keyed on event id TestBillingWebhookConcurrentReplayAppliesOnce
D1 HIGH Snapshot CTE wipes lock columns on past_due transitions conditional preservation in ApplySubscriptionSnapshot + user analog TestApplySubscriptionSnapshotPreservesGraceLockOnPastDue (+ recovery + user-side mirror)
D2 LOW No charge.refunded handler new dispatch + MarkInvoiceRefunded query + enum + UI surface TestBillingWebhookChargeRefundedMarksInvoiceRefunded (+ unknown invoice + standalone refund)
D3 MED Two subscriptions per customer silently overwrite guardSubscriptionOverwrite rejects different-sub apply TestBillingWebhookGuardRefusesSecondSubscriptionForSameCustomer
D4 MED No stale-event detection (reverse-order corruption) last_event_at column + IsBillingEventStaleForPrincipal + handler guard + post-apply touch TestBillingWebhookDropsStaleEvent
D5 LOW customer.subscription.deleted for unknown sub 5xx's log + 200 no-op so Stripe stops retrying TestBillingWebhookSubscriptionDeletedForUnknownSubIsNoOp
C1 HIGH require_signed_commits unreachable from UI form field + template checkbox + sqlc plumbing covered by TestSettingsBranches_UserKindEnforceFlipPreventForcePushBlocks table
C2 HIGH Advanced BP gate fires on wrong inputs rewire predicate to fire on prevent_*, signing, AND status-checks table test
C3 MED Multi-reviewer deny copy indistinguishable thread reviewer count → required-reviewers-multi-upgrade(-pro) codes TestSettingsBranches_UserKindEnforceFlipMultiReviewerBlocks
C4 MED Notice copy says "Team" / "organization" on user-tier denies user-kind variants (-pro suffix) with /settings/billing href TestSettingsBranches_UserKindEnforceFlipRequiredReviewersBlocksSingle (Pro copy)
C5 LOW profilePinsRemaining hard-codes cap; under-counts for Pro thread entitled cap into the function covered by existing profile_test suite + no regression

Authorization audit (Agent B) found no cross-tenant data leak, no invoice-scoping bleed, and no deny-shape 500 reachable from production paths. Two err→500 branches in the entitlement gate remain (in RequirePrincipalFeature and evaluateBranchProtectionFeature) but are defended against by the AFTER-INSERT seed triggers on both billing-state tables — pgx.ErrNoRows for a valid principal is dead code in production.

Pre-existing failures NOT introduced by PRO08

These were noted across prior sprints and remain open:

  • TestPrivateCollaborationExpansionEnforcesFreeLimitAndTeamUnlimited
  • TestPrivateCollaborationUsageCountsEffectivePrivateAccess
  • TestRepoPrivateVisibilityCountsRepoSpecificGrants

The fourth pre-existing failure (TestSettingsBranchesAllowsDowngradedOrgToRemoveAdvancedSettings) was fixed opportunistically while in the branch-protection cluster (seed fixture needed AllowedPusherUserIds: []int64{} to satisfy the NOT NULL constraint).

Go/no-go: GA

GO, conditional on the operator completing the Stripe Dashboard checklist documented in docs/internal/runbooks/stripe-billing.md under "Subject resolution chain" and "Per-feature enforcement flags" sections. Production binary needs redeploy to pick up:

  • Migrations 0077 (last_event_at) + 0078 (refunded enum + column)
  • The 13 code fixes above

Deploy steps:

  1. ssh root@shithub.sh "sudo -u shithub /usr/local/bin/shithubd migrate up" — applies 0077 + 0078
  2. Restart shithubd-web.service and shithubd-worker.service
  3. Verify with: SELECT version_id FROM goose_db_version ORDER BY version_id DESC LIMIT 1 → expect 78
  4. Configure Stripe env vars per the runbook (still in test mode until ratified)
  5. Run the smoke checklist in runbooks/stripe-billing.md#smoke-test
  6. Flip enforce flags per feature after 7-day report-only soak

No production customers exist today (SELECT plan, count(*) FROM users WHERE deleted_at IS NULLfree: 2; same for orgs), so the deploy carries near-zero risk to existing customers — there are none on Pro/Team plans.

View source
1 # Billing and paid organizations
2
3 shithub's first paid surface is organization billing. This document
4 records the product and engineering contract that the PAYMENTS sprint
5 series implements.
6
7 The current implementation already has the important shape for paid
8 organizations: `orgs.plan` is an enum with `free`, `team`, and
9 `enterprise`; organizations own repositories; organization members and
10 teams exist; branch protection and PR review gates exist; Actions has
11 schema for org/repo secrets, variables, and artifacts. Billing must
12 turn that substrate into a fair hosted service without taxing
13 public/open-source collaboration.
14
15 ## Product contract
16
17 As of 2026-05-12, GitHub's public pricing page presents Free at
18 `$0`, Team at `$4/user/month`, and Enterprise starting at
19 `$21/user/month`. shithub follows the same mental model but removes
20 Copilot/AI promises from the paid-org offering.
21
22 Initial decisions:
23
24 - Free organizations remain self-serve.
25 - Team is `$4` per active organization member per month.
26 - Active organization members, including owners, count as paid seats.
27 - Team has no launch trial.
28 - Enterprise is a visible contact-sales stub, not self-serve.
29 - Stripe Billing is the first payment processor.
30 - PayPal, manual invoices, SAML, SCIM, LDAP, enterprise account
31 hierarchy, and contracts are deferred.
32 - Self-serve organization creation presents `/organizations/plan` as
33 the canonical plan selector. Choosing Team creates the organization
34 and immediately redirects the owner to hosted Stripe Checkout.
35
36 The fairness rule is explicit: public/open-source collaboration should
37 stay generous. Paid gates focus on private collaboration, hosted cost,
38 advanced organization controls, and support expectations.
39
40 ## Pricing copy rules
41
42 Pricing and onboarding pages must describe only features shithub can
43 actually deliver on the hosted service. Before changing pricing copy,
44 refresh the official GitHub pricing source and the Stripe Billing docs
45 because both are time-sensitive inputs.
46
47 Rules for paid-org copy:
48
49 - Do not mention Copilot, AI agents, AI code review, or AI quotas.
50 - Do not promise SAML, SCIM, LDAP, managed users, audit exports, data
51 residency, compliance attestations, contracts, or custom support
52 until the matching implementation sprint ships.
53 - Do not advertise Packages, Pages, Wikis, Projects, Actions minutes,
54 or storage quotas until those surfaces have enforcement and usage
55 accounting.
56 - Use upgrade language for unavailable Team features instead of hiding
57 existing data. Downgrades preserve configuration and make gated
58 settings read-only where possible.
59 - Keep public/open-source collaboration generous in both copy and
60 enforcement. Avoid copy that makes public repositories feel like a
61 second-class Free tier.
62 - Enterprise is a contact-sales stub in v1. It should collect interest
63 without promising contractual features.
64
65 ## Entitlement matrix
66
67 | Capability | Free | Team | Enterprise stub |
68 | --- | --- | --- | --- |
69 | Public org repositories | Included | Included | Contact sales |
70 | Basic private org repositories | Included | Included | Contact sales |
71 | Org members and invitations | Included | Billed by active member | Contact sales |
72 | Effective private org collaborators | Up to 3 | Unlimited while active/in grace | Contact sales |
73 | Visible teams | Included | Included | Contact sales |
74 | Secret teams | Upgrade | Included | Contact sales |
75 | Basic branch protection | Included | Included | Contact sales |
76 | Advanced private-repo branch protection | Upgrade | Included | Contact sales |
77 | Required reviewers on private org repos | Upgrade | Included | Contact sales |
78 | CODEOWNERS review | Deferred | Deferred | Deferred |
79 | Org-level Actions secrets | Upgrade | Included | Contact sales |
80 | Org-level Actions variables | Upgrade | Included | Contact sales |
81 | Actions minutes | Low quota once metered | Higher quota once metered | Contact sales |
82 | Actions artifacts/storage | Low quota once metered | Higher quota once metered | Contact sales |
83 | Packages storage | Deferred until Packages is active | Deferred until Packages is active | Deferred |
84 | Pages/Wikis/Projects | Do not promise until shipped | Do not promise until shipped | Deferred |
85 | Audit log export | Deferred | Deferred | Later Enterprise feature |
86 | SAML/SCIM/managed users | Deferred | Deferred | Later Enterprise feature |
87 | Data residency/compliance | Deferred | Deferred | Later Enterprise feature |
88 | Billing support | Basic instance support | Billing support after runbook exists | Contact sales |
89
90 ## Pro v1 user-tier matrix (PRO07)
91
92 Pro is the user-tier paid plan (single seat, $4/month). PRO01 ratified
93 the v1 feature set; PRO07 wires the enforcement matrix. CODEOWNERS is a
94 registered placeholder with a no-op enforce path until the parser ships.
95
96 | Capability | Free | Pro |
97 | --- | --- | --- |
98 | Public/private personal repos | Included | Included |
99 | Required reviewers on private personal repos | Upgrade | Included |
100 | Multi-reviewer (>1 approvals) on private personal repos | Upgrade | Included |
101 | Advanced branch protection on private personal repos | Upgrade | Included |
102 | CODEOWNERS review | Deferred | Deferred |
103 | Profile pins | 6 | 100 |
104
105 Multi-reviewer is **not** a separate feature constant — the numeric
106 threshold lives in the deny payload of `FeatureRequiredReviewers`.
107
108 ### Per-feature enforcement flags
109
110 PRO05 plumbed user-kind report-only logging through every gating site.
111 PRO07 lights up the gates one feature at a time via
112 `billing.enforce.*` in the operator's config. Defaults are all false
113 (report-only). Each flag is a one-way deploy that operators can roll
114 back without code changes.
115
116 | Config key | Gate site | Default |
117 | --- | --- | --- |
118 | `billing.enforce.user_required_reviewers` | `internal/web/handlers/repo/settings_branches.go` | false |
119 | `billing.enforce.user_advanced_branch_protection` | `internal/web/handlers/repo/settings_branches.go` | false |
120 | `billing.enforce.user_profile_pins_beyond_free` | `internal/web/handlers/profile/pins.go` | false |
121
122 Rollout discipline:
123
124 1. Deploy with all flags false. Run the report-only telemetry query
125 for 7 days. Confirm zero unexpected user-kind would-denies.
126 2. Flip one feature flag in staging. Soak 7 days.
127 3. Flip the same feature flag in production.
128 4. Repeat per feature.
129
130 PRO07's pitfall doc explicitly forbids enforcing a feature without an
131 unenforce path. New gating sites land with their own flag; do not
132 share flags across features.
133
134 ### Downgrade preservation
135
136 `users.plan = 'free'` after cancellation grandfather's existing gated
137 state — required-reviewer rules, profile pins above 6, advanced flags
138 on existing rules. The gate refuses to **create** new gated state on
139 Free, but never deletes prior configuration. This is the same
140 contract as the org-tier downgrade.
141
142 ## Current capability audit
143
144 Already present and safe to gate:
145
146 - Organizations with `plan` and `billing_email`.
147 - Organization members, owner role, and invitations.
148 - Teams, including `privacy='secret'`.
149 - Branch protection rules and required review counts.
150 - PR review and reviewer-request substrate.
151 - Org/repo Actions secrets and variables schema.
152
153 Present but missing enforcement or metering:
154
155 - Storage quota type exists, but quota persistence and enforcement are
156 incomplete.
157 - Actions minutes, artifacts, and object usage need accounting before
158 paid limits can be promised.
159 - Packages storage cannot be sold until the Packages sprint is active
160 and quota enforcement exists.
161
162 Deferred:
163
164 - SAML, SCIM, LDAP, enterprise account hierarchy, audit-log export, data
165 residency, compliance promises, and custom support SLAs.
166 - Copilot/AI features are intentionally outside shithub's paid-org
167 product.
168
169 ## Billing architecture
170
171 Stripe is the payment source of truth. shithub is the entitlement source
172 of truth.
173
174 The billing implementation should add a local billing domain that stores
175 only Stripe IDs and payment summaries, never card data. Webhooks update
176 local subscription state after signature verification. Policy and
177 request handlers read local billing/entitlement state and must not call
178 Stripe in hot paths.
179
180 Required local concepts:
181
182 - Stripe customer per billable organization.
183 - Subscription state per organization.
184 - Subscription item ID for seat quantity sync.
185 - Immutable webhook receipts with unique provider event IDs.
186 - Invoice/payment summaries for UI.
187 - Seat snapshots for auditability.
188 - Billing grace/lock state derived from processed subscription events.
189
190 PAYMENTS SP02 adds these as local database tables:
191
192 - `org_billing_states` stores the organization billing projection used
193 by entitlement checks.
194 - `billing_seat_snapshots` records active and billable seat counts over
195 time.
196 - `billing_invoices` stores invoice/payment summaries for billing UI.
197 - `billing_webhook_events` stores immutable provider event receipts for
198 idempotent webhook processing.
199
200 New organizations receive a Free billing state from a database trigger,
201 and the migration backfills existing organizations as Free. Subscription
202 snapshot writes also keep `orgs.plan` synchronized as the
203 human-facing summary.
204
205 PAYMENTS SP03 adds the first Stripe operator contract:
206
207 - `billing.enabled=false` keeps paid-org flows disabled while retaining
208 the local billing tables.
209 - `billing.stripe.secret_key`, `billing.stripe.webhook_secret`, and
210 `billing.stripe.team_price_id` are required before Stripe routes are
211 mounted.
212 - Checkout success, Checkout cancel, and Billing Portal return URLs may
213 be overridden explicitly; otherwise the web layer derives absolute
214 organization URLs from `auth.base_url`.
215 - `billing.grace_period` controls how long failed-payment states may
216 remain unlocked before paid entitlements are cut off.
217 - When billing routes are mounted, `/settings/organizations` links
218 owner-managed organizations to their billing page's plan comparison.
219 When billing is disabled, that action stays visibly unavailable
220 instead of linking to an unmounted route.
221
222 The operator enablement flow is documented in
223 [`runbooks/stripe-billing.md`](./runbooks/stripe-billing.md).
224
225 PAYMENTS SP04 adds the self-serve onboarding flow:
226
227 - `/organizations/plan` is the canonical plan picker.
228 - Free setup creates the organization locally without Stripe.
229 - Team setup creates the organization, creates or reuses the Stripe
230 customer, counts billable seats, and redirects directly to hosted
231 Stripe Checkout.
232 - Checkout success and cancel returns render shithub pages. Success
233 tells the owner that activation waits for webhook processing; cancel
234 keeps the organization on Free and offers a retry path.
235
236 PAYMENTS SP05 adds the local entitlement boundary. Product code must ask
237 `internal/entitlements` for feature decisions instead of inspecting
238 `orgs.plan` directly. The package derives access from
239 `org_billing_states`, understands billing-good-standing states, and
240 returns upgrade metadata for user-facing handlers.
241
242 PAYMENTS SP06 wires the first Team gates:
243
244 - Secret teams require Team to create. Existing secret teams remain
245 visible to authorized viewers after downgrade; owners can remove
246 members and repository grants, but adding members or granting more
247 repository access is blocked until Team billing is active again.
248 - Required reviewers and advanced status-check branch protection are
249 Team-only for private organization repositories. Public organization
250 repositories keep those safety controls available on Free.
251 - Downgraded private organization repositories may delete protection
252 rules or submit a rule update that clears the gated review/check
253 settings.
254 - Org-level Actions secrets and variables require Team for create or
255 update in both HTML settings and REST API routes. Delete stays
256 available so owners can clean up gated configuration after downgrade.
257 - Org-level Actions secrets and variables API routes require
258 organization owner or site-admin access before entitlement checks.
259
260 PAYMENTS SP07 completes the first self-serve billing settings surface:
261
262 - `GET /organizations/{org}/settings/billing` is owner-only and is
263 linked from organization settings when Stripe Billing is configured.
264 - The page shows current local plan state, subscription status, payment
265 source summary, recent Stripe-synced invoice snapshots, and actionable
266 banners for past-due, grace-period, canceled, scheduled-cancel, and
267 billing-action-needed states.
268 - Seat accounting is shown as three separate values: current active
269 members, billable seats from the latest local billing state, and
270 pending invitations. Pending invitations are explicitly not billed
271 until accepted.
272 - Team organizations manage payment method, invoices, cancellation, and
273 downgrade through Stripe Billing Portal. shithub never collects card
274 data directly and downgrades continue to preserve paid configuration
275 as read-only data.
276 - Normal organization owners do not see raw Stripe customer,
277 subscription, or subscription-item IDs. Site admins see a debug panel
278 with those IDs and the latest locally recorded webhook receipt state.
279
280 PAYMENTS SP06a adds the first private-collaboration limit:
281
282 - Free organizations may have up to 3 unique humans with effective
283 access to at least one private organization repository.
284 - Team organizations with active, trialing, or in-grace subscriptions
285 have unlimited private collaborators.
286 - The effective private-collaborator set counts org owners, direct
287 collaborators on private org repos, and team members who inherit a
288 private repo grant through direct team membership or one-level parent
289 team inheritance. Plain org members do not count unless they gain
290 private repo access through one of those paths.
291 - Public repository collaboration never counts toward the limit.
292 - Downgrades preserve existing access even when the org is already over
293 the Free limit, but writes that add a new effective private
294 collaborator are blocked until the org upgrades or removes access.
295 - Creating/importing a private org repository and changing an org repo
296 from public to private are blocked on Free when the resulting private
297 collaborator set would exceed the limit.
298 - Cleanup writes remain available: removing org members, team members,
299 direct collaborators, team repo grants, and gated configuration must
300 not require Team.
301
302 ## Entitlement architecture
303
304 Paid feature checks must live behind a central entitlement package, not
305 as scattered `orgs.plan` checks in handlers.
306
307 `make lint-org-plan` enforces this boundary. Schema/sqlc plumbing may
308 store and scan the plan value, but product behavior should ask the
309 entitlement package whether a feature key is available.
310
311 The package entrypoint is `entitlements.ForOrg(ctx, deps, orgID)`,
312 which loads the local `org_billing_states` projection and returns a
313 request-scoped entitlement set. Callers then use `CanUse(feature)` for
314 feature decisions and `Limit(name)` for paid limit metadata. The legacy
315 `CheckOrgFeature` helper is a thin wrapper for handlers that need only
316 one feature. These calls are deterministic and never call Stripe.
317
318 Expected feature keys:
319
320 - `org.secret_teams`
321 - `org.advanced_branch_protection`
322 - `org.required_reviewers`
323 - `org.actions_org_secrets`
324 - `org.actions_org_variables`
325 - `org.private_collaboration_limit`
326 - `org.storage_quota`
327 - `org.actions_minutes_quota`
328
329 Authorization and entitlement are separate gates. A user must have both
330 the policy permission and the paid entitlement for gated writes. Denials
331 must preserve existing `policy.Maybe404` behavior where existence leaks
332 matter.
333
334 Entitlement outcomes are:
335
336 - Free organizations receive `upgrade_required` for Team features.
337 - Team organizations with `active` or `trialing` subscriptions receive
338 the feature.
339 - Team organizations in `past_due` remain usable only while their local
340 grace window has not expired.
341 - Team organizations with incomplete, unpaid, paused, canceled, or
342 expired-grace billing receive `billing_action_needed`.
343 - Enterprise remains a contact-sales stub and receives
344 `enterprise_contact_sales` until a later Enterprise sprint defines a
345 real feature set.
346
347 Handler upgrade helpers map paid-feature denials to HTTP 402 metadata
348 and a billing-settings path, but handlers must still perform normal
349 authorization first and preserve 404 masking for private resources.
350
351 ## Downgrade behavior
352
353 Downgrades must preserve customer data. Moving from Team to Free should
354 not delete teams, secrets, variables, branch rules, or review settings.
355 Existing gated resources become read-only where possible. Users can
356 remove gated configuration, but cannot create or expand it until the
357 organization upgrades again.
358
359 ## Open questions for implementation
360
361 - Exact Free and Team quota numbers for Actions and storage. These must
362 come from real host-cost estimates before SP08.
363
364 ## Source references
365
366 - GitHub pricing: `https://github.com/pricing`
367 - GitHub plans docs:
368 `https://docs.github.com/en/get-started/learning-about-github/githubs-plans`
369 - Stripe Billing: `https://docs.stripe.com/billing`
370 - Stripe pricing models:
371 `https://docs.stripe.com/products-prices/pricing-models`
372
373 ## PRO08 GA audit closure
374
375 **Audit dates**: 2026-05-13 (run + remediation in a single sprint).
376
377 **Scope**: PRO04 Stripe adapter + PRO05 entitlements Principal +
378 PRO06 user-tier billing settings + PRO07 Pro v1 feature gates.
379
380 **Methodology**: four parallel security/correctness audit agents
381 ran read-only inspections against the PRO07 tip plus the deployed
382 host (`shithub.sh`). Findings were triaged per-bug; HIGH and
383 MEDIUM severity gaps were fixed inline on the `payments-pro/08-pro-ga-audit`
384 branch with locking tests. LOW-severity items were either fixed or
385 documented per the user directive "we don't want to take any chances
386 with payments."
387
388 ### Findings + remediation status
389
390 | # | Severity | Finding | Fix | Test |
391 |---|---|---|---|---|
392 | A1 | MED | `guardPriceKindMatch` bypassable on empty `Items` | refuse empty-items when prices configured | `TestBillingWebhookGuardRefusesEmptyItemsWhenPricesConfigured` |
393 | A2 | HIGH | `(subject_kind, subject_id)` never written to receipts | `SetWebhookEventSubjectForPrincipal` after every resolve; `ListFailedWebhookEvents` operator query | `TestSetWebhookEventSubjectForPrincipalRecordsAuditTrail`, `TestListFailedWebhookEventsReturnsErroredAndStuckEntries` |
394 | A3 | MED | Concurrent dup-replay TOCTOU race | session-scoped advisory lock keyed on event id | `TestBillingWebhookConcurrentReplayAppliesOnce` |
395 | D1 | HIGH | Snapshot CTE wipes lock columns on past_due transitions | conditional preservation in `ApplySubscriptionSnapshot` + user analog | `TestApplySubscriptionSnapshotPreservesGraceLockOnPastDue` (+ recovery + user-side mirror) |
396 | D2 | LOW | No `charge.refunded` handler | new dispatch + `MarkInvoiceRefunded` query + enum + UI surface | `TestBillingWebhookChargeRefundedMarksInvoiceRefunded` (+ unknown invoice + standalone refund) |
397 | D3 | MED | Two subscriptions per customer silently overwrite | `guardSubscriptionOverwrite` rejects different-sub apply | `TestBillingWebhookGuardRefusesSecondSubscriptionForSameCustomer` |
398 | D4 | MED | No stale-event detection (reverse-order corruption) | `last_event_at` column + `IsBillingEventStaleForPrincipal` + handler guard + post-apply touch | `TestBillingWebhookDropsStaleEvent` |
399 | D5 | LOW | `customer.subscription.deleted` for unknown sub 5xx's | log + 200 no-op so Stripe stops retrying | `TestBillingWebhookSubscriptionDeletedForUnknownSubIsNoOp` |
400 | C1 | HIGH | `require_signed_commits` unreachable from UI | form field + template checkbox + sqlc plumbing | covered by `TestSettingsBranches_UserKindEnforceFlipPreventForcePushBlocks` table |
401 | C2 | HIGH | Advanced BP gate fires on wrong inputs | rewire predicate to fire on prevent_*, signing, AND status-checks | table test |
402 | C3 | MED | Multi-reviewer deny copy indistinguishable | thread reviewer count → `required-reviewers-multi-upgrade(-pro)` codes | `TestSettingsBranches_UserKindEnforceFlipMultiReviewerBlocks` |
403 | C4 | MED | Notice copy says "Team" / "organization" on user-tier denies | user-kind variants (`-pro` suffix) with `/settings/billing` href | `TestSettingsBranches_UserKindEnforceFlipRequiredReviewersBlocksSingle` (Pro copy) |
404 | C5 | LOW | `profilePinsRemaining` hard-codes cap; under-counts for Pro | thread entitled cap into the function | covered by existing profile_test suite + no regression |
405
406 Authorization audit (Agent B) found **no** cross-tenant data leak,
407 no invoice-scoping bleed, and no deny-shape 500 reachable from
408 production paths. Two `err→500` branches in the entitlement gate
409 remain (in `RequirePrincipalFeature` and `evaluateBranchProtectionFeature`)
410 but are defended against by the AFTER-INSERT seed triggers on
411 both billing-state tables — `pgx.ErrNoRows` for a valid principal
412 is dead code in production.
413
414 ### Pre-existing failures NOT introduced by PRO08
415
416 These were noted across prior sprints and remain open:
417
418 - `TestPrivateCollaborationExpansionEnforcesFreeLimitAndTeamUnlimited`
419 - `TestPrivateCollaborationUsageCountsEffectivePrivateAccess`
420 - `TestRepoPrivateVisibilityCountsRepoSpecificGrants`
421
422 The fourth pre-existing failure
423 (`TestSettingsBranchesAllowsDowngradedOrgToRemoveAdvancedSettings`)
424 was fixed opportunistically while in the branch-protection cluster
425 (seed fixture needed `AllowedPusherUserIds: []int64{}` to satisfy
426 the NOT NULL constraint).
427
428 ### Go/no-go: GA
429
430 **GO**, conditional on the operator completing the Stripe Dashboard
431 checklist documented in `docs/internal/runbooks/stripe-billing.md`
432 under "Subject resolution chain" and "Per-feature enforcement flags"
433 sections. Production binary needs redeploy to pick up:
434
435 - Migrations 0077 (`last_event_at`) + 0078 (`refunded` enum + column)
436 - The 13 code fixes above
437
438 Deploy steps:
439
440 1. `ssh root@shithub.sh "sudo -u shithub /usr/local/bin/shithubd migrate up"` — applies 0077 + 0078
441 2. Restart `shithubd-web.service` and `shithubd-worker.service`
442 3. Verify with: `SELECT version_id FROM goose_db_version ORDER BY version_id DESC LIMIT 1` → expect `78`
443 4. Configure Stripe env vars per the runbook (still in test mode until ratified)
444 5. Run the smoke checklist in `runbooks/stripe-billing.md#smoke-test`
445 6. Flip enforce flags per feature after 7-day report-only soak
446
447 **No production customers exist today** (`SELECT plan, count(*) FROM users WHERE deleted_at IS NULL``free: 2`; same for orgs), so the deploy carries near-zero risk to existing customers — there are none on Pro/Team plans.