Uncommitted changes on integration-plugins begin collapsing two auth planes — org-scoped integration_connections and user-scoped plugin_connections — into one connections table and one declarative registry. This walks every surface that moved: schema, API routes, tRPC, desktop, web, CLI, and the plugin manifests themselves.
Every first-party integration — Google, Linear, Slack, Notion, Sentry, Microsoft Teams, GitHub — used to authenticate twice: once as an org-scoped integration_connections row behind a hand-written connect/callback route pair, and again as a user-scoped plugin_connections row behind a second, generic-but-separate OAuth pipeline. This diff does not touch triggers, webhooks, or the MCP dispatch path yet — it fuses the auth plane only, one table and one route pair standing in for both.
The merge is additive at the edges, subtractive in the middle: every provider-specific webhook handler, sync job, and trigger matcher named in the earlier design proposal is untouched in this diff — syncMailbox.ts, processDelivery.ts, jobs/renew-watches all still exist and still do what they did. What changed under them is where a token comes from: connections instead of integration_connections, resolved through packages/trpc/src/lib/connectors instead of a per-provider helper.
Grouped by the eight places a change had to land. Status reads against the working tree: new a file that did not exist before, removed deleted outright, renamed moved and edited, modified edited in place.
One migration: a connections table replaces plugin_connections, and automation_events repoints its foreign key at it.
| Path | Status | Note |
|---|---|---|
| packages/db/drizzle/0112_add_connectors_registry.sql | new | Creates connections, drops plugin_connections, widens ingest.webhook_events.provider from enum to text |
| packages/db/src/schema/schema.ts | modified | Adds the connections table def; owner-kind partial unique indexes for org vs. user scope |
| packages/db/src/schema/plugins.ts | modified | Drops the pluginConnections table definition |
| packages/db/src/schema/relations.ts | modified | Relation graph repointed at connections |
| packages/db/src/schema/ingest.ts | modified | webhook_events.provider enum → text |
The three plugin-specific OAuth routes are retired wholesale; a parallel connectors route tree replaces them. The seven provider callback/webhook routes keep their paths and just read the new table.
| Path | Status | Note |
|---|---|---|
| app/api/connectors/[connector]/connect/route.ts | new | 93 lines — generic authorize redirect for any registry entry |
| app/api/connectors/callback/[connector]/route.ts | new | 106 lines — generic token exchange + identity probe + upsert |
| app/api/connectors/[connector]/client-metadata/route.ts | new | Serves the CIMD document for RFC 9728 dynamic client registration |
| app/api/plugins/[plugin]/connect/route.ts | removed | Superseded by connectors/[connector]/connect |
| app/api/plugins/callback/[plugin]/route.ts | removed | Superseded by connectors/callback/[connector] |
| app/api/plugins/[plugin]/client-metadata/route.ts | removed | Superseded by connectors/[connector]/client-metadata |
| app/api/integrations/{google,linear,slack,notion,sentry,microsoft-teams}/… | modified | 21 route/lib files — callback and webhook handlers repointed at connections; shape and paths unchanged |
| src/lib/integrations/upsertConnection.ts | removed | 146 lines — logic absorbed into packages/trpc/src/lib/connectors/upsert.ts |
| src/lib/oauth-state.ts | modified | State payload gains a connector slug alongside the plugin name |
A new lib/connectors replaces the OAuth logic that used to live under router/plugins; a new router/connectors is the tRPC surface the two UIs call.
| Path | Status | Note |
|---|---|---|
| lib/connectors/index.ts | new | 444 lines — registry lookup, method resolution, endpoint resolution |
| lib/connectors/upsert.ts | new | 195 lines — one connection write path for both OAuth and API-key methods |
| lib/connectors/lookup.ts | new | 134 lines — orgConnection / user-scoped reads, newest-first, live-only by default |
| lib/connectors/refresh.ts | new | 98 lines — ensureFreshConnection, throws UnrefreshableConnectionError instead of silently going stale |
| lib/connectors/discovery.ts | renamed | from router/plugins/discovery.ts — RFC 9728 protected-resource + authorization-server metadata discovery |
| lib/connectors/client-identity.ts | renamed | from router/plugins/client-identity.ts — dynamic client registration (RFC 7591 / CIMD) |
| router/connectors/index.ts | new | catalog, get, status, connectApiKey, disconnect procedures |
| router/plugins/oauth.ts + oauth.test.ts | removed | 433 + 172 lines — the plugin-only OAuth implementation this replaces |
| router/plugins/refresh.ts | removed | 55 lines — folded into lib/connectors/refresh.ts |
| router/plugins/{connections,plugins,manifest,dispatch}.ts | modified | Read connection state through the new lib instead of owning it |
| router/integration/… (18 files) | modified | google, linear, notion, sentry, slack, microsoft-teams routers and their auth/refresh/state helpers, all repointed at connections |
What used to be OAuth config scattered across seven plugin.json files is now one declarative document, validated by a discriminated union over four method types.
| Path | Status | Note |
|---|---|---|
| packages/shared/src/connectors/connectors.json | new | 7 connectors — google, linear, notion, slack, sentry, microsoft_teams, github |
| packages/shared/src/connectors/index.ts | new | Zod schema for oauth2 / api_key / app_install / admin_consent methods |
| packages/shared/src/plugins/manifests.generated.ts | modified | −212 lines — per-plugin auth blocks dropped now that connectors.json owns them |
| Path | Status | Note |
|---|---|---|
| components/ConnectorSection/{ConnectorSection,useConnector}.tsx | new | Promoted to top-level renderer/components — used from both the plugin detail page and the automation trigger editor |
| components/ConnectorSection/components/ConnectConnectorDialog.tsx | new | Shared connect modal, API-key form included |
| plugins/$pluginName/.../PluginDetail.tsx | modified | Swaps its bespoke PluginConnections block for <ConnectorSection slug={plugin.connector}> |
| plugins/$pluginName/.../PluginConnections/* | removed | 237-line component + hook, superseded |
| plugins/hooks/usePluginConnections/* | removed | 84 lines — superseded by useConnector |
| automations/components/TriggersEditor/TriggersEditor.tsx | modified | Missing-connection trigger rows now open ConnectConnectorDialog in place, rather than routing away |
| automations/components/TriggerSentence/TriggerSentence.tsx | modified | Takes an onConnect callback to trigger the dialog above |
| Path | Status | Note |
|---|---|---|
| app/(app)/(page)/connect/[connector]/page.tsx | new | Marketing/marketplace-side connect page, one route for every registry entry |
| .../connect/[connector]/components/ConnectorConnect/* | new | 176-line client component — same catalog + status + connect flow as the desktop dialog |
| Path | Status | Note |
|---|---|---|
| packages/cli/src/commands/plugins/{connect,install,list}/command.ts | modified | superset plugins connect now resolves against the connector registry, not a per-plugin auth block |
| packages/cli/src/lib/plugins/{connection-ref,inputs,marketplace}.ts | modified | Connection reference resolution follows the same slug |
| plugins/{gmail,linear,notion,slack,github}/plugin.json | modified | Each drops its own auth block and gains one line: "connector": "<slug>" |
Three things disappear rather than move, because the merged table makes them redundant:
plugin.json's hand-written auth: [...] block — authorization URL, token URL, scopes, identity probe, bind spec — is gone. That configuration now lives once, in connectors.json, keyed by the same slug the manifest references.router/plugins/oauth.ts (433 lines) and its test file are deleted outright, not refactored — the generic connect/callback pair under connectors replaces what it did for every provider at once.PluginConnections and its usePluginConnections hook are gone; ConnectorSection is the one component both the plugin detail page and the trigger editor now mount.The design proposal committed to this branch (plans/20260909-one-plugin-surface.html) lays out four phases. Reading the diff against it:
One connections table, backfilled indexes for org vs. user scope, one revoke path. This diff, essentially in full.
Stateless MCP dispatch collapsing remote and bundled plugin servers behind one handle() function. dispatch.ts is touched only incidentally here, not restructured.
discovery.ts (RFC 9728) and client-identity.ts (dynamic client registration) both moved into lib/connectors and are wired into the new callback route. legacyOauth as a distinct manifest shape, and the RFC 9207 iss check called out as missing, aren't visibly addressed yet.
Generic ingress, generic trigger matcher, attributes-based filters. Every webhook route (gmail/push, linear/webhook, notion/webhook, sentry/webhook, Teams subscriptions) keeps its own provider-specific verify-and-normalize logic in this diff — only its token source changed.
From the proposal's blocker list (B1–B8), the two that this diff's scope leaves untouched and that later phases will have to answer:
A connection minted through connectors/callback for, say, Linear's MCP server is not automatically valid against api.linear.app/graphql. Product code that still calls provider REST/GraphQL APIs directly — getLinearClient(), Teams' Graph calls — needs the credentials-per-resource design from §05 of the proposal before this can be relied on universally. Not yet visible in the diff.
Google's watch renewal (jobs/renew-watches), Notion and Teams' per-workspace registration are still bespoke cron jobs against the old per-provider shape, just now reading tokens from connections. The manifest-driven events.lifecycle block from Phase 4 would fold these into one shared cron; until then, each stays a distinct thing to keep renewing correctly.