I was pairing on a Jira setup and watched the customer open three tabs to mint an Atlassian API token, copy it into a password manager, paste it into our form, then go back to revoke an older token they'd created for a PoC months earlier. The whole dance took longer than the investigation they wanted Annie to run once Jira was connected.

That flow is gone for the common path. Integrations now ship a Connect with… button that runs a normal OAuth authorization-code grant (OIDC-adjacent where the vendor layers identity on top of OAuth 2.0). You approve in the vendor UI, we store encrypted tokens, and Annie keeps working without you babysitting expiry.

What's live

Over the last week we wired OAuth connect across the integrations that were still stuck on paste-a-token. Jira and Confluence share one Connect with Atlassian grant (Atlassian 3LO). Linear recommends OAuth and still accepts API keys. Notion uses a public connection with workspace install and page picker.

Sentry runs OAuth with PKCE on sentry.io, and pasted auth tokens remain for self-hosted hosts. GitLab restores Connect with GitLab for gitlab.com while self-managed keeps PAT. incident.io goes through their hosted remote MCP (mcp.incident.io), with API keys still available for automation.

We also hardened Wiz client-credentials caching so MCP calls stop re-authing on every request. PagerDuty, Datadog, and Guru are next once their OAuth apps clear vendor review.

The part that isn't the button

The button was the easy half. The half that kept me in SELECT FOR UPDATE land was token refresh under vendors that rotate refresh tokens on every exchange.

Atlassian is the sharp case. Every refresh returns a new access token and a new refresh token; if two workers refresh the same row without locking, the second write invalidates the first and the connection dies with invalid_grant until someone reconnects. Linear is similar: access tokens expire after 24 hours and refresh tokens rotate. Notion and Sentry follow the same shape with their own quirks (Notion mints a unique pair per authorization; Sentry wants PKCE).

So the backend owns refresh for every OAuth integration:

1. Near expiry (or on an agent-gateway nudge), lock the credential row.

2. Exchange the refresh token with the vendor.

3. KMS-encrypt the new access token and the new refresh token, write them atomically.

4. Hand only the decrypted access token to agent-gateway for the MCP call.

A five-minute cron covers the quiet periods. invalid_grant flips the connection to a reconnect status instead of silently failing Annie mid-investigation. Existing personal-token connections keep working; we didn't wipe anyone's PAT when OAuth landed.

The annoying bit on our side: each vendor took its own table shape and callback path before the pattern settled. Atlassian got a fresh atlassian_oauth_connections table; Linear and Notion extended the existing credential tables. I wish we'd locked the schema on day one of ANY-1377 instead of converging after the third integration.

What you do differently on Monday

Open the integration page, click Connect with…, approve the scopes in the vendor UI, pick the project, space, org, or pages Annie should see. Revoke from the vendor console when someone leaves the team. You can still paste a token when OAuth isn't an option (self-managed GitLab, self-hosted Sentry, service accounts). That path is intentional, not a half-finished migrate.

If a connection drops, the UI asks you to reconnect rather than leaving Annie with a dead bearer. That was the failure mode that used to show up as a vague MCP timeout after someone rotated a token in a vault and forgot to update Anyshift.

Setup docs live under each integration at docs.anyshift.io.