# Each user's account

It's common to want the agent to act as the person it's talking to. For instance, "Which Linear issues are assigned to me?" only works with that person's own Linear.

## Let each user connect

Add a connector by its name:

```yaml title="agents/assistant/agent.yaml"
name: assistant # an internal assistant for your team
mode: text
llm: llm-fast
connectors:
  - linear # each user signs in to their own Linear
```

The user signs in at Linear, and the agent never sees their password.

A test in the terminal has no signed-in user, so it leaves `linear` out and tells you so. Test it in the Stream Dashboard, where you are the signed-in user:

```sh
curl -fsSL https://getstream.io/cli/beta/install.sh | bash # the agents commands are in the beta CLI
getstream agents test assistant --sync --dashboard
```

And ask: "Which Linear issues are assigned to me?". The reply shows a **Connect Linear** button. Sign in, and the same reply goes on to list your issues.

## Connect in your app's chat

In your own app, the reply asks the same way: the call's step waits for the user to sign in, and the AI components show the **Connect Linear** card on it. Give each session a `returnUrl` on your page's origin, where the sign-in comes back, and an approver that runs it:

```tsx
const returnUrl = `${window.location.origin}/connected.html`;
const session = await api.agent("assistant").sessions.create({ returnUrl });

const approver: AIToolApprover = {
  userId: "alice",
  clientId: AIClientIdentity.installId(),
  decide: async (call, allowed) => session.decide(call.id, { allowed }),
  authorizer: createPopupAuthorizer({
    returnUrl,
    decline: async (call) => session.declineLogin(call.id),
  }),
};

<AIConversation approver={approver} messages={messages} />;
```

The return page reports how the sign-in went and closes the popup:

```html title="connected.html"
<script type="module">
  import { completeConnectorSignIn } from "stream-chat-react/ai-components";
  completeConnectorSignIn();
</script>
```

Connect opens the sign-in in a popup, and the same reply goes on once the user is in. Not now calls `session.declineLogin`, so the call runs nothing and the agent says the user didn't connect. On iOS and Android, name your app's own scheme in reverse domain form, such as `com.example.assistant:/connected`, as the session's `returnURL` or `returnUrl`, and the sign-in card opens the page in the system's sign-in sheet. The [React](https://github.com/GetStream/Vision-Agents/tree/accelerate/examples/voice_agents/react_assistant), [Swift](https://github.com/GetStream/Vision-Agents/tree/accelerate/examples/voice_agents/swift_assistant) and [Android](https://github.com/GetStream/Vision-Agents/tree/accelerate/examples/voice_agents/android_assistant) assistant samples do this.

A sign-in comes back only to the `return_url` the session was created with, so a conversation started on a phone finishes its sign-ins on that phone.

## Connect from your app

Your app can also show its own Connect button. Your backend starts the sign-in, your frontend opens it in a popup, and the popup comes back to your page:

```mermaid
sequenceDiagram
  participant B as Your backend
  participant F as Your frontend
  participant R as Stream router
  actor U as User
  participant P as Provider
  B->>R: POST /v1/agents/connections
  R-->>B: id, status pending
  B->>R: POST /v1/agents/connections/{id}/authorizations
  R-->>B: launch_url, handoff_token
  B-->>F: launch_url, handoff_token
  F->>R: open launch_url in a popup
  R-->>F: va.connector.oauth.ready
  F->>R: va.connector.oauth.handoff with handoff_token
  R->>P: the popup goes to the provider
  U->>P: signs in and approves
  P->>R: the popup comes back to the router
  R-->>F: the popup goes to return_url?connection_id=...&status=connected
```

### Your backend: create the connection

`$STREAM_ACCELERATION_URL` is your router's URL, and `$SERVER_TOKEN` is a [server token](https://getstream.io/agents/docs/javascript/auth/): an HS256 JWT of `{"server": true}` signed with your API secret. `X-Stream-User-Id` and `owner.user_id` name the same user:

```bash
curl -X POST "$STREAM_ACCELERATION_URL/v1/agents/connections" \
  -H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
  -H "Authorization: Bearer $SERVER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Stream-User-Id: alice" \
  -d '{"connector_id": "linear", "owner": {"type": "user", "user_id": "alice"}}'
```

It returns the connection, `pending`, with its `id`. Keep the `id` with your user: you need it to check the status and to reconnect.

### Your backend: start the sign-in

Use that `id` as `$CONNECTION_ID`:

```bash
curl -X POST "$STREAM_ACCELERATION_URL/v1/agents/connections/$CONNECTION_ID/authorizations" \
  -H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
  -H "Authorization: Bearer $SERVER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Stream-User-Id: alice" \
  -d '{"return_url": "https://app.example.com/settings/integrations"}'
```

It returns a `launch_url` and a `handoff_token` for your frontend. The sign-in expires 10 minutes later, and both calls are server-side only.

`return_url` must be `https`, or `http` on `localhost`. Without `return_url`, the sign-in goes back to the dashboard.

### Your frontend: open the popup

Open the popup from a page on the `return_url`'s origin: the popup takes the handoff token from that origin only. Wait for the popup's ready message, then post it the token:

```js
const popup = window.open(
  "about:blank",
  "connect",
  "popup,width=520,height=720",
);
const { launchUrl, handoffToken } = await startConnect(); // your backend makes both calls
const routerOrigin = new URL(launchUrl).origin;
popup.location = launchUrl;
window.addEventListener("message", (event) => {
  if (event.source !== popup || event.origin !== routerOrigin) return;
  if (event.data?.type !== "va.connector.oauth.ready") return;
  popup.postMessage(
    { type: "va.connector.oauth.handoff", handoff_token: handoffToken },
    routerOrigin,
  );
});
```

The handoff token works once.

### Your return page: tell the opening page

When the sign-in ends, the popup lands on your `return_url`:

```text
https://app.example.com/settings/integrations?connection_id=...&status=connected
```

`status` is `connected` or `failed`. A user who cancels at the provider comes back with `failed`.

Pass `status` on to the opening page, for instance with a same-origin `BroadcastChannel`, then close the popup.

### Your backend: check the status

```bash
curl "$STREAM_ACCELERATION_URL/v1/agents/connections/$CONNECTION_ID" \
  -H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
  -H "Authorization: Bearer $SERVER_TOKEN" \
  -H "X-Stream-User-Id: alice"
```

| `status`                | What it means                                                       | What to do              |
| ----------------------- | ------------------------------------------------------------------- | ----------------------- |
| `pending`               | Nobody has signed in yet. It stays `pending` after a failed sign-in | Start a sign-in again   |
| `connected`             | The user signed in, and the agent can use the account               | Nothing                 |
| `needs_reauthorization` | The provider no longer accepts the credential                       | [Reconnect](#reconnect) |

## Reconnect

[Start a new sign-in](#your-backend-start-the-sign-in) on the same connection, and open its `launch_url` the same way. Until the user finishes, the connection stays as it was.

## OAuth clients

Stream's client needs no setup, so we recommend starting with it. Set your own client when you go to production. The [catalog](https://getstream.io/agents/docs/javascript/connectors/#catalog) shows which connectors take one.

Your app's client is set on the connector under **Library > Connectors** (OAuth client), or from your backend. Its redirect URI is `$STREAM_ACCELERATION_URL/v1/agents/connectors/oauth/callback`. We encrypt the secret and never return it.

```bash
curl -X PUT "$STREAM_ACCELERATION_URL/v1/agents/connectors/google_calendar/oauth-client" \
  -H "api_key: $STREAM_API_KEY" -H "stream-auth-type: jwt" \
  -H "Authorization: Bearer $SERVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "...", "client_secret": "..."}'
```

## Going further

- [Connectors](https://getstream.io/agents/docs/javascript/connectors/): Whose account, the catalog, and the keys of a connector entry.
- [Shared account](https://getstream.io/agents/docs/javascript/connectors/shared-account/): One account the app owns, for every conversation.
- [Troubleshooting](https://getstream.io/agents/docs/javascript/connectors/troubleshooting/): Errors you may see, why, and what to do.

---

For the most recent version of this documentation, visit [https://getstream.io/agents/docs/javascript/connectors/each-user-account/](https://getstream.io/agents/docs/javascript/connectors/each-user-account/).