feat(controlplane): add self-hosted billing support#2653
Open
mekilis wants to merge 49 commits into
Open
Conversation
Route self-hosted bootstrap flows through the billing strategy, enforce billing-manage access for authenticated bootstrap calls, and hide payment provider config on self-hosted billing config responses.
Add plan feature fields back to Convoy billing models so Overwatch-provided feature sets are preserved in /billing/plans responses and available to the dashboard manage-plan flow.
Preserve plan description text returned by Overwatch in Convoy billing responses so manage-plan clients receive the same display copy as the upstream billing API.
Require billing authorizer presence for self-hosted bootstrap mutations and prevent nil strategy panics by returning safe defaults/errors in billing config and usage handlers, with focused regression coverage.
Distinguish mode-filtered plan emptiness from fetch failures and guide cloud-linked users to manage plans in Convoy Cloud instead of showing a misleading retry-only state.
Apply organisation host override directly to runtime config, scope billing health checks to configured cloud/licensed modes, and remove an unused billing client field from the billing handler/tests.
Drop redundant MockBillingClient field assignments in authz-gated tests so govet unusedwrite checks pass consistently in CI lint.
Remove upstream body preview from billing non-JSON ServiceError messages so handler responses do not expose internal diagnostics.
Use subscription computed entitlements to overlay effective feature values on the current-plan comparison column and display baseline defaults when values are overridden.
Hide workspace slug UI and dialog outside managed cloud mode so self-hosted org settings no longer expose cloud-only slug actions.
Prevent self-hosted paths from mutating organisation disabled_at by guarding both request-time status updates and worker registration/execution to cloud mode only.
Refine manage-plan pricing cards with centered emphasis, tooltip-based billing cadence notes, and symmetric monthly/yearly normalization so mixed plan intervals remain easy to compare.
Remove a redundant IsSelfHosted check in login refresh gating while preserving the existing cloud and license-key behavior.
Prevent org billing requests from falling back to the instance license key and add coverage for org-specific and no-license paths.
- Route licensedStrategy.GetTaxIDTypes through licenseKeyFor so org-scoped callers no longer prefer the instance license key. - Dedupe cloud-billing email backfill goroutines per org via sync.Map so a burst of dashboard hits cannot fan out repeated GetOrganisation/UpdateOrganisation calls against the billing service. - Log unlicensed GetOrganisation fetch failures and GetBillingConfig org resolution failures instead of silently degrading the response. - Document the resolveKey contract in services/refresh_license_data.go and the dev-only Origin reflection in SetupCORS so future readers know why staging/production never reflect the request Origin. - Bubble unknown login ErrCode values up as 500 with the service message rather than flattening them into a generic "Login failed".
CreateOrganisationService.Run was firing RunBillingOrganisationSync whenever hostForBilling resolved to a non-empty string, and hostForBilling falls back to cfg.Host. That meant signing up a new org on a licensed or unlicensed self-hosted instance was silently transmitting the org name and owner email to the configured billing endpoint, breaking the self-hosted privacy contract. Restrict the call to cfg.IsCloud() so only managed cloud deployments perform the outbound sync.
The previous ensureOrganisationInBilling helper always sourced Host from the server config; the new public CreateOrganisation surface accepted it from the request body, letting a caller bind a billing organisation to an arbitrary Host. Override orgData.Host with h.A.Cfg.Host before forwarding to the billing client to restore that invariant.
The CONVOY_BILLING_ORGANISATION_HOST override only updated the local cfg fed into appHandler.cfg, leaving the shared types.APIOptions.Cfg at the unoverridden host. CreateOrganisation pins orgData.Host to h.A.Cfg.Host, so without this assignment the pin silently falls back to the original cfg.Host whenever the env override is configured. Mirror the override into a.Cfg.Host alongside appHandler.cfg. checkBillingAccess and checkBillingCreateAccess shared the UID match, authorizer-presence, and PermissionBillingManage policy check; only the org resolution differed. Extract that tail into authorizeBillingManage so the two entry points cannot drift.
…data Gate org-scoped license features and login refresh on inactive subscriptions, clear license_data on cancel and when billing reports no active subscription. Resolve Overwatch license-billing calls from CONVOY_LICENSE_KEY only on non-cloud. Wire OrgBilling into refresh deps and add tests.
Require active subscription plus resolved plan for cached org license_data. Refresh billing config after cancel, reload overview for subscribe-only state, checkout polling, and invoice catchup; fix team sidebar after license fetch.
Stop awaiting bootstrapSubscriptionPromise from loadOrganisationData when loadBillingData runs inside bootstrapOrganisation (async deadlock after checkout). Drop teamAccessResolved gate so Team lock state tracks license features without waiting on a separate resolved flag.
Hoist PrimaryInstanceAccess for login reuse. When CheckOrgLimit fails, allow one org if the user has no memberships and matches the same access rule as sign-in. Wire UserRepo into create-org call paths including bootstrap.
…drop Show ring+check for current plan and filled check when selected (selected wins). Clear plan selection when clicking outside the card row unless checkout is loading.
poll after checkout when session_id is missing but pre-checkout baseline exists; wait for real plan change vs stored baseline; clear nosession dedupe keys on cleanup; tier-aware upgrade/downgrade/switch labels; remove redundant delayed billing refresh timer.
include the active organisation header for self-hosted billing bootstrap calls, allow it in dev cors, and add explicit handler error logs so local upstream failures are easier to diagnose.
keep the post-verification guidance focused on setting the license key while clarifying that users can re-run verification if needed.
remove dead cloud-only guard from CreateOrganisation and require licensed strategy org-scoped calls to match the license billing context organisation, with focused tests.
remove org-binding enforcement for licensed strategy billing calls so instance-scoped self-hosted licenses continue to work across the dashboard billing flows.
document that licensed self-hosted billing uses an instance-scoped license key, clarify cloud-mode auth expectations in route tests, and mask user emails in self-hosted billing error logs.
abbf101 to
7173dd6
Compare
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.
Summary
Note
High Risk
High risk because it refactors billing mode detection (cloud vs self-hosted) and rewires multiple authenticated control-plane endpoints (billing, license features, SSO/login) with new authorization and tenancy checks that could impact access control and subscription gating.
Overview
Adds a first-class billing mode concept (
cloudvslicensedvsunlicensed) and removes the oldBilling.Enabledflag; billing now always has a URL default and mode is derived fromBilling.APIKey/LicenseKey.Refactors dashboard/control-plane billing to use a new
billing.Strategy(APIOptions.Billing) and expands the billing HTTP client with self-hosted and license-key–scoped endpoints plus safer error handling (sanitized URL logging, typedServiceError, subscription decoding).Introduces self-hosted purchase bootstrap endpoints (
/ui/billing/self_hosted/register_email+verify_email), tightens billing/plan/tax/catalog andGetLicenseFeaturesorg-scoped access with membership/role checks, and adjusts route auth rules so/ui/license/featuresis public only for unlicensed self-hosted requests.Updates SSO/admin-portal and post-login license refresh flows to be cloud-only where appropriate (and to pass
OrgBilling), clears orglicense_dataon subscription cancel, and updates tests/integration helpers accordingly.Reviewed by Cursor Bugbot for commit 7173dd6. Bugbot is set up for automated code reviews on this repo. Configure here.