Skip to main content

End-user connections

By default, connections belong to your project: every agent request uses the same Linear workspace or Slack account. End-user connections add a second kind of connection, owned by one user of your application. Each connection carries a resource ID, an opaque key you choose for that user, and the platform only serves it to requests asserting the same key.

With end-user connections, an agent resolves your project's shared connections plus the calling user's own, so "create an issue" lands in that user's Linear workspace rather than yours.

Before you start, follow the Connect setup to configure your project.

Identify your users
Direct link to Identify your users

End-user connections require authenticated users. Configure server auth and map each authenticated user to a stable resource ID:

src/mastra/index.ts
import { Mastra } from '@mastra/core'

export const mastra = new Mastra({
server: {
auth: {
authenticateToken: async token => verifyToken(token),
// The resource ID is the key end-user connections are stored under.
// Use a stable, non-reusable identifier for the user.
mapUserToResourceId: user => user.id,
},
},
})

The server derives the resource ID after authentication, so a client can't claim another user's key.

Scope agent tools to the calling user
Direct link to Scope agent tools to the calling user

Pass resourceId to tools() inside a dynamic tools function. The agent then resolves global connections plus that user's connections on every call:

src/mastra/agents/assistant.ts
import { Agent } from '@mastra/core/agent'
import { MASTRA_RESOURCE_ID_KEY } from '@mastra/core/request-context'
import { tools } from '@mastra/connect'

export const assistant = new Agent({
id: 'assistant',
name: 'assistant',
instructions: 'You help the user manage their own tasks.',
model: 'openai/gpt-5-mini',
tools: ctx =>
tools({
providers: ['linear'],
resourceId: () => ctx.requestContext?.get(MASTRA_RESOURCE_ID_KEY) as string | undefined,
}),
})

Calling tools() per request is cheap: identical options share one internal state, with snapshots cached per resource ID. When the resource ID resolves to undefined, the agent gets global connections only, the same behavior as before.

If both a global and a user connection exist for one provider, the agent sees both through the usual multi-connection handling.

Every platform call made under a resource ID asserts it, and the platform rejects mismatches, so one user's connection can never serve another user's request.

Let users connect their accounts
Direct link to Let users connect their accounts

userConnectRoutes() mounts the API your frontend needs: a provider catalog, the caller's connections, connect-session creation, status polling, disconnect, and a hosted connect page. Register it on the server:

src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { userConnectRoutes } from '@mastra/connect'

export const mastra = new Mastra({
server: {
auth: {
authenticateToken: async token => verifyToken(token),
mapUserToResourceId: user => user.id,
},
apiRoutes: [
...userConnectRoutes({
// Required: only these providers are exposed to end users.
providers: ['linear', 'slack'],
}),
],
},
})

The routes do no authentication of their own. Your server's auth pipeline authenticates each request, and the routes are gated on the resource ID it derives, so a request without one gets a 401. All platform calls happen server-side with your platform token. The browser never sees it.

To start a connection, send the user to the hosted page:

GET /connect/linear?returnTo=/settings/integrations

The page shows a Connect button, opens the provider's authorization flow in a popup, polls until the connection is active, and then redirects back to returnTo with connectionId and status query parameters. returnTo accepts relative paths only, unless you allowlist origins with returnTo.allowedOrigins.

The hosted page is a plain browser navigation: it carries no Authorization header, and the page's own status calls send only same-origin cookies. It therefore requires auth that can authenticate the request itself, such as a session cookie. authenticateToken receives the raw request as its second argument:

src/mastra/index.ts
const auth = {
authenticateToken: async (token: string, request: Request) => {
// JSON routes send a bearer token.
if (token) return verifyToken(token)
// The hosted page sends no token: authenticate the session cookie.
return verifySessionCookie(request.headers.get('cookie'))
},
mapUserToResourceId: (user: { id: string }) => user.id,
}

With token-only auth, the hosted page returns a 401. The JSON routes still work: call them with the bearer token and build your own UI instead.

Prefer your own UI? Call the JSON routes directly. See the route reference.

Clean up after deleted users
Direct link to Clean up after deleted users

When you delete a user from your application, delete their connections too. Each mounted DELETE /connect/connections/:id call disconnects one of the caller's own connections; for offboarding, use the platform API's bulk cleanup for the user's resource ID from your backend.

On this page