refactor(web,desktop): Linear becomes the first of provider-keyed integrations (integrations 1/5)
What
Dean: "Let's add Notion, Slack, Sentry, Google Docs/Drive. And let's pull out the settings to a new Integrations tab." This is the first PR of that rollout. It turns the Linear connection into the first of several provider-keyed integrations, and Linear keeps working exactly as before. It also does Dean's follow-up: "clean up the confirmation page for Linear connected (and any other services). It should show the same as successful sign in with GitHub. And don't have so much text in the primary or subtext."
-
One provider table.
apps/web/server/hosted/integrations.tsgives each provider:- the env variable its Vercel Connect connector UID is read from;
- its hosted MCP server and what the model is told about it;
- the fixed name its nested calls are stored under in a transcript.
The shared vocabulary is
packages/hosted/src/integration-wire.ts:INTEGRATION_PROVIDER(as const), the statuses, and the landing statuses. Each later provider PR adds one entry, four one-line eve connection files, a mark, and a privacy paragraph. -
Store. A new
integration_connectiontable keyed(user_id, provider)(migration0096). The migration copies everylinear_connectionrow in as Linear's, so a connected account stays connected and a sign-in under way still settles.linear_connectionis no longer read. It stays for one deploy, so the build still serving during this deploy keeps working, and the next PR's migration drops it. -
Routes. One group (
server/integrations-app.ts) replaces the four Linear routes, and the desktop moves to it in the same PR, since no desktop has shipped:GET /api/account/integrationsanswers{ integrations: { linear: "connected" | … } }.POST /api/account/integrations/{provider}/connect.POST /api/account/integrations/{provider}/disconnect.GET /api/account/integrations/connected?state=…is the one Connect callback for every provider; the state names both the account and the provider.
The rewrite moves
{provider}into aproviderquery, the way a plan's id is moved. An unknown provider gets 404. -
Agents. Every agent registers each provider's connection through one factory,
integrationConnection(provider, read)inserver/hosted/integration-connection.ts. Eachconnections/linear.ts(eve, coder, and both workers) is still one line. Sentences, admission, the token read, and the "a Disconnect wins over an in-flight token" behaviour are unchanged. -
Desktop.
- Gateway methods and acts are now
integrations.status/integrations.connect {provider}/integrations.disconnect {provider}, throughHostedIntegrationsClientandcompose-integrations.ts. IntegrationRowtakes a provider, anduseIntegrationStatuses()reads every provider once.- Linear's row is drawn where it was (Settings › General). Moving it to Settings › Integrations is the next PR.
- The sign-in address is still opened by the host and never reaches a window.
- Gateway methods and acts are now
-
Account deletion revokes the grant at every configured provider at Vercel Connect. This generalizes #2299's revoke (
revokeIntegrationGrants), and PRIVACY.md keeps #2299's wording. -
The connected page (
integration-connected.html, formerlylinear-connected.html) now dresses like the page a GitHub sign-in leaves the browser on: the provider's mark, an arrow, Luke's face, one short line, and one shorter one. "Linear connected / You can return to Luke." Each failure is just as short, e.g. "Linear not connected / Return to Luke to try again." The pairing graphic, previously duplicated insign-in.tsxandgithub-installed.tsx, is now oneconnection-graphic.tsx. Linear's mark path data comes fromdesign/generate-surface-shared.mjs(Simple Icons' trace, CC0), next to GitHub's and Google's.
Tests
integrations-app.test.ts: the route goldens, moved and re-recorded underLUKE_UPDATE_FIXTURES=1infixtures/integrations-route/. The diff renames paths and nests status underintegrations. New cases: an unknown provider and a missing provider are 404.integration-store.test.ts(PGlite): a sign-in is found by its state, completes once, and expires only when connected. One account never reads another's rows, and the rows go with the account.integration-migration.test.ts: rows written tolinear_connectionbefore 0096 read back connected / pending-by-state after it. It fails without the copy statement.integration-connection.test.ts,integration-agents.test.ts,linear-transcript.test.ts: ported unchanged in behaviour.account-app.test.ts: a delete revokes every provider's grant; a failed revoke still deletes; a deployment with no connector asks Connect nothing.compose-integrations.test.ts: an unknown provider is refused unasked.integration-client.test.ts: a provider missing from the answer is no answer.integration-acts.test.ts,integration-row.test.ts: ported to the provider-keyed acts.
./scripts/check.sh exits 0.
Screenshots
These are rendered in headless Chrome on Linux. The GitHub page is the desktop's loopback page (accountLoopbackPage), rendered with the strings a GitHub sign-in gives it.
| GitHub sign-in (existing) | Linear connected (new) |
|---|---|
![]() | ![]() |
| Not completed | Expired link (no provider known) |
|---|---|
![]() | ![]() |
Mac validation
CI could not verify the Mac UI: no CI job builds the Mac app. ./scripts/verify.sh was not run, because this is a Linux cloud VM; LUKE-159 is the manual Mac pass. The desktop changes here are wiring only, and the Settings row looks the same.
🤖 Generated with Claude Code



