feat(integrations): suspend GitHub and Linear sync for organizations that leave Pro
What
One predicate decides whose integrations are live, and one hourly job brings the providers into line with it.
organizationSyncs(@superset/trpc/sync-policy): a paid subscription that is active, or was canceled less than 7 days ago. Never-paid and abandoned-checkout orgs have nothing that ended, so they get no grace; churned orgs get a week with everything intact. An idle rule later is anand(...)here and nothing else moves.POST /api/integrations/jobs/suspend(hourly, QStash schedule already created): suspends the GitHub App installation and revokes Linear tokens (POST /oauth/revoke, row markedsync_suspended) for every org the policy ices; unsuspends and backfills GitHub for every org it readmits. Each provider stops at its first rate-limit response and the next run carries on.- The Linear accept path, fan-out, token refresh, connect-time backfills and the manual GitHub sync button read the same predicate.
onSubscriptionCompletequeues both initial-syncs; GitHubinitial-synclifts only a suspension the App placed (suspended_by.type === "Bot"). - The GitHub webhook route does no per-event plan lookup: suspension makes GitHub stop delivering, which costs the route nothing.
- Removes the Hookdeck dual path from the Linear webhook (the trial lost in September; Linear posts straight to the API; repo secret already deleted).
Why
Measured on prod 2026-09-19: webhook bookkeeping is ~68% of DB time. GitHub PR-sync churn over 24h was 45% never-paid orgs, 30% paying, 14% churned, 11% abandoned checkout; 1,320 of 1,658 active Linear connections belong to non-paying orgs, and Tasks — the only consumer of that sync — is behind the paywall. GitHub sync is unused until cloud ships, in Pro. Ignoring deliveries at the route still costs an invocation per event; suspending at the provider costs nothing per event.
First run: ~2,771 installations suspended, ~1,304 Linear connections revoked; 10 and 9 in the 7-day grace.
Verification
- lint, typecheck,
check:i18n(16 translations written), api 218/218, trpc suite green apart from 7deriveSandboxCredentialscases that fail only under the full turbo run and pass alone — that directory is untouched here. - Prod query of the predicate against
github_installations/integration_connectionsmatches the counts above. - Not yet proven: that revoking every token for a Linear org stops its webhook. Follows from Linear creating one webhook per authorizing org; watch
ingest.webhook_eventsfor a revoked org's id after the first run.
Landmine (not this PR)
Prod's connections table is empty; live rows are in integration_connections. Main's code (#7317) reads connections and no backfill exists, so the Linear half of this cron — and the Linear webhook on main — sees zero rows until that backfill lands.
Summary by cubic
Suspends GitHub and Linear sync for organizations that leave Pro, and gates every accept path on the same policy.
A single predicate, organizationSyncs, decides whose integrations are live: an active paid subscription, or one canceled less than 7 days ago (never-paid and abandoned-checkout orgs get no grace). An hourly QStash job suspends GitHub App installations and revokes Linear tokens for everyone else, and unsuspends plus backfills GitHub when the policy readmits an org. Free orgs no longer get backfills at connect time; the subscription-complete hook queues both backfills on upgrade.
- GitHub sync resumes in place; Linear tokens are revoked, so coming back means reconnecting via the Connect flow.
- The job runs under a 240s budget, stops at the first rate-limit response, and re-checks the policy before each Linear revoke so an org that pays mid-run keeps its connection.
- Resuming queues the backfill before lifting the suspension, so a failed publish leaves the row for the next run.
- Removes the Hookdeck dual path from the Linear webhook (Linear now posts straight to the API), along with
HOOKDECK_SIGNING_SECRETand its verification helpers.
Migration
- Prod's
connectionstable is empty; live Linear rows are inintegration_connections. No backfill exists, so the Linear half of the suspend job reads zero rows until that lands. This is a pre-existing gap on main, not introduced by this PR.
Summary by CodeRabbit
- New Features
- GitHub synchronization is limited to eligible paid plans, with a clear Pro-plan message when starting a sync without access.
- GitHub and Linear integrations are reconciled with subscription status. Syncs can pause when eligibility ends and resume when it returns, with an initial sync queued on resumption.
- Bug Fixes
- GitHub and Linear sync jobs and webhook processing skip organizations that aren’t eligible to sync.
- Linear webhooks require Linear signatures and reject malformed or invalidly signed requests.
- Unknown GitHub installations no longer trigger webhook processing, except for installation lifecycle events.
- Documentation
- Added localized translations for the GitHub Pro-plan requirement.