Skip to content

feat(controlplane): add self-hosted billing support#2653

Open
mekilis wants to merge 49 commits into
mainfrom
selfhosted-billing
Open

feat(controlplane): add self-hosted billing support#2653
mekilis wants to merge 49 commits into
mainfrom
selfhosted-billing

Conversation

@mekilis
Copy link
Copy Markdown
Collaborator

@mekilis mekilis commented May 13, 2026

Summary

  • Add self-hosted billing configuration, API handlers, billing client strategies, and license refresh support.
  • Update dashboard billing/settings flows for self-hosted billing and instance-level licensing.
  • Allow the first org-less user through single-user login and clear stale org state for org-less users.

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 (cloud vs licensed vs unlicensed) and removes the old Billing.Enabled flag; billing now always has a URL default and mode is derived from Billing.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, typed ServiceError, subscription decoding).

Introduces self-hosted purchase bootstrap endpoints (/ui/billing/self_hosted/register_email + verify_email), tightens billing/plan/tax/catalog and GetLicenseFeatures org-scoped access with membership/role checks, and adjusts route auth rules so /ui/license/features is 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 org license_data on 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.

@mekilis mekilis changed the title feat(billing): add self-hosted billing support feat(controlplane): add self-hosted billing support May 13, 2026
Comment thread api/api.go Outdated
Comment thread api/api.go
Comment thread internal/dataplane/worker.go Outdated
Comment thread api/handlers/billing.go
Comment thread api/api.go Outdated
Comment thread api/api.go
Comment thread internal/pkg/billing/strategy.go
Comment thread api/handlers/license.go Outdated
Comment thread api/handlers/billing.go
Comment thread api/handlers/license.go Outdated
Comment thread config/config.go
Comment thread api/handlers/project.go Outdated
Comment thread api/handlers/project.go
Comment thread api/handlers/billing_self_hosted.go Outdated
Comment thread api/handlers/billing.go
Comment thread api/api.go
Comment thread api/api.go Outdated
Comment thread internal/dataplane/worker.go Outdated
Comment thread internal/pkg/billing/client.go
Comment thread api/handlers/billing_self_hosted.go
Comment thread api/handlers/billing.go
Comment thread api/handlers/billing.go
Comment thread api/handlers/auth.go Outdated
Comment thread api/api.go Outdated
Comment thread api/api.go Outdated
Comment thread api/handlers/billing.go
Comment thread api/handlers/auth.go
Comment thread api/handlers/billing_self_hosted.go
Comment thread api/api.go Outdated
Comment thread api/handlers/billing_self_hosted.go
Comment thread api/api.go Outdated
Comment thread api/api.go
Comment thread api/handlers/billing_self_hosted.go
Comment thread api/handlers/billing.go Outdated
Comment thread api/handlers/billing.go
Comment thread internal/pkg/billing/client.go Outdated
Comment thread api/handlers/billing.go
Comment thread api/handlers/auth.go
Comment thread internal/pkg/billing/strategy.go Outdated
Comment thread services/create_organisation.go Outdated
Comment thread api/handlers/billing.go
Comment thread api/api.go
Comment thread api/handlers/billing.go
mekilis added 29 commits May 20, 2026 13:40
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.
@mekilis mekilis force-pushed the selfhosted-billing branch from abbf101 to 7173dd6 Compare May 20, 2026 12:41
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.

1 participant