Working-tree audit · integration-plugins

What the connector merge touches

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.

2026-09-14 branch integration-plugins base HEAD (uncommitted) plan plans/20260909-one-plugin-surface.html
146
paths touched
+1653
lines added
−3871
lines removed
22
new files
11
deleted files
3
renamed
8
surface areas

01 The shape of the change

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.

Before: two separate connection tables and OAuth route pairs feeding seven provider integrations. After: one connections table and one declarative connectors.json feeding the same providers through one route pair. before integration_connections org-scoped · pgEnum provider plugin_connections user-scoped · per install connect / callback ×7 connect / callback 7 providers, 2 routes each google · linear · slack · notion sentry · teams · github after connections owner_kind: user | org · one row per grant connectors.json declarative registry · packages/shared connect / callback 7 providers, 1 route pair + discovery.ts for RFC 9728 consumers packages/trpc/router/connectors · catalog · status · connectApiKey · disconnect desktop ConnectorSection web /connect/[connector] CLI · superset plugins connect / install 7 integration route handlers, unchanged shape, now read/write connections instead of the old tables
Two auth planes, seven duplicated route pairs → one table, one registry, one route pair, five consumers.

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.

02 Surface inventory

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.

Database & schema

One migration: a connections table replaces plugin_connections, and automation_events repoints its foreign key at it.

PathStatusNote
packages/db/drizzle/0112_add_connectors_registry.sqlnewCreates connections, drops plugin_connections, widens ingest.webhook_events.provider from enum to text
packages/db/src/schema/schema.tsmodifiedAdds the connections table def; owner-kind partial unique indexes for org vs. user scope
packages/db/src/schema/plugins.tsmodifiedDrops the pluginConnections table definition
packages/db/src/schema/relations.tsmodifiedRelation graph repointed at connections
packages/db/src/schema/ingest.tsmodifiedwebhook_events.provider enum → text
API routes — apps/api

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.

PathStatusNote
app/api/connectors/[connector]/connect/route.tsnew93 lines — generic authorize redirect for any registry entry
app/api/connectors/callback/[connector]/route.tsnew106 lines — generic token exchange + identity probe + upsert
app/api/connectors/[connector]/client-metadata/route.tsnewServes the CIMD document for RFC 9728 dynamic client registration
app/api/plugins/[plugin]/connect/route.tsremovedSuperseded by connectors/[connector]/connect
app/api/plugins/callback/[plugin]/route.tsremovedSuperseded by connectors/callback/[connector]
app/api/plugins/[plugin]/client-metadata/route.tsremovedSuperseded by connectors/[connector]/client-metadata
app/api/integrations/{google,linear,slack,notion,sentry,microsoft-teams}/…modified21 route/lib files — callback and webhook handlers repointed at connections; shape and paths unchanged
src/lib/integrations/upsertConnection.tsremoved146 lines — logic absorbed into packages/trpc/src/lib/connectors/upsert.ts
src/lib/oauth-state.tsmodifiedState payload gains a connector slug alongside the plugin name
tRPC — connector plumbing

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.

PathStatusNote
lib/connectors/index.tsnew444 lines — registry lookup, method resolution, endpoint resolution
lib/connectors/upsert.tsnew195 lines — one connection write path for both OAuth and API-key methods
lib/connectors/lookup.tsnew134 lines — orgConnection / user-scoped reads, newest-first, live-only by default
lib/connectors/refresh.tsnew98 lines — ensureFreshConnection, throws UnrefreshableConnectionError instead of silently going stale
lib/connectors/discovery.tsrenamedfrom router/plugins/discovery.ts — RFC 9728 protected-resource + authorization-server metadata discovery
lib/connectors/client-identity.tsrenamedfrom router/plugins/client-identity.ts — dynamic client registration (RFC 7591 / CIMD)
router/connectors/index.tsnewcatalog, get, status, connectApiKey, disconnect procedures
router/plugins/oauth.ts + oauth.test.tsremoved433 + 172 lines — the plugin-only OAuth implementation this replaces
router/plugins/refresh.tsremoved55 lines — folded into lib/connectors/refresh.ts
router/plugins/{connections,plugins,manifest,dispatch}.tsmodifiedRead connection state through the new lib instead of owning it
router/integration/… (18 files)modifiedgoogle, linear, notion, sentry, slack, microsoft-teams routers and their auth/refresh/state helpers, all repointed at connections
Shared — the connector registry

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.

PathStatusNote
packages/shared/src/connectors/connectors.jsonnew7 connectors — google, linear, notion, slack, sentry, microsoft_teams, github
packages/shared/src/connectors/index.tsnewZod schema for oauth2 / api_key / app_install / admin_consent methods
packages/shared/src/plugins/manifests.generated.tsmodified−212 lines — per-plugin auth blocks dropped now that connectors.json owns them
Desktop
PathStatusNote
components/ConnectorSection/{ConnectorSection,useConnector}.tsxnewPromoted to top-level renderer/components — used from both the plugin detail page and the automation trigger editor
components/ConnectorSection/components/ConnectConnectorDialog.tsxnewShared connect modal, API-key form included
plugins/$pluginName/.../PluginDetail.tsxmodifiedSwaps its bespoke PluginConnections block for <ConnectorSection slug={plugin.connector}>
plugins/$pluginName/.../PluginConnections/*removed237-line component + hook, superseded
plugins/hooks/usePluginConnections/*removed84 lines — superseded by useConnector
automations/components/TriggersEditor/TriggersEditor.tsxmodifiedMissing-connection trigger rows now open ConnectConnectorDialog in place, rather than routing away
automations/components/TriggerSentence/TriggerSentence.tsxmodifiedTakes an onConnect callback to trigger the dialog above
Web
PathStatusNote
app/(app)/(page)/connect/[connector]/page.tsxnewMarketing/marketplace-side connect page, one route for every registry entry
.../connect/[connector]/components/ConnectorConnect/*new176-line client component — same catalog + status + connect flow as the desktop dialog
CLI & plugin manifests
PathStatusNote
packages/cli/src/commands/plugins/{connect,install,list}/command.tsmodifiedsuperset plugins connect now resolves against the connector registry, not a per-plugin auth block
packages/cli/src/lib/plugins/{connection-ref,inputs,marketplace}.tsmodifiedConnection reference resolution follows the same slug
plugins/{gmail,linear,notion,slack,github}/plugin.jsonmodifiedEach drops its own auth block and gains one line: "connector": "<slug>"

03 What got retired outright

Three things disappear rather than move, because the merged table makes them redundant:

04 Where this sits on the roadmap

The design proposal committed to this branch (plans/20260909-one-plugin-surface.html) lays out four phases. Reading the diff against it:

1
Connections
merged

One connections table, backfilled indexes for org vs. user scope, one revoke path. This diff, essentially in full.

2
One server ABI
not started

Stateless MCP dispatch collapsing remote and bundled plugin servers behind one handle() function. dispatch.ts is touched only incidentally here, not restructured.

3
Discovery-first auth
partial

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.

4
Events
not started

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.

05 Still open

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:

B2
Audience-bound tokens

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.

B1
Webhook lifecycle stays hand-rolled

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.