---
name: vinkius-multi-user-chatbot
description: 'Teaches the multi-user consumer chatbot pattern on @vinkius/connect: one chat route serves thousands of humans, each addressed by their own opaque external_id and each with their own isolated connectors, credentials and capabilities. Explains the actor model, on-demand account connection, scoped capability loading, safe write execution and the not-connected product moment. Use when building a per-user assistant, a "Connect your account" flow, or a chat tool loop with @vinkius/connect. Prerequisite: vinkius-connect-core.'
license: Apache-2.0
metadata:
  author: Vinkius
  package: "@vinkius/connect"
  pattern: "user = human end-user"
  docs: "https://vinkius.com/learn/connect-sdk/build-multi-user-chatbot"
---

# Multi-user consumer chatbot

Build on the machinery taught by vinkius-connect-core. This skill teaches the one decision this pattern makes and what it implies.

## The actor model

Here the user is the literal thing: a human with an account in your product. Their stable login key (a database id, a hash, whatever your auth already mints) becomes the `external_id`. Never invent a second identity system: one user row, one id, passed to `vinkius.user()` per request. The id is opaque to Vinkius on purpose: emails, names and profiles never leave your database, and that is the customer-relationship boundary you keep.

What the isolation buys you: two users connecting the *same* GitHub integration get completely separate connections, credentials and capabilities, with no path across the boundary and no isolation logic in your code. Swapping the id swaps every tool, token and permission, by construction.

## How the pieces fit

- One `Vinkius` client in one module, reused by every request. Construction is free; the client holds configuration, not a user.
- Authenticate first, then identify: resolve the caller from your session, pass that id to the SDK. A `userId` read from a request body is a security hole, not a shortcut.
- The chat handler per turn: load that user's capabilities (scoped with `include` to the connectors your product uses), convert to tools with the adapter matching your model, call the model, execute whichever tool it chose, feed results back, loop with a step budget.
- Connecting is a product moment, not an error: `capabilities()` only lists `ready` connectors, so a missing account simply does not appear. Detect it (the capability you need is absent), read `connector(slug).status()`, and show your own "Connect GitHub" affordance. The throw path (`ConnectorNotConnectedError`) exists only for operations on a handle you never connected; `QuotaError` carries the plan `upgradeUrl`, map it to your own upgrade page.

## Rules that keep this safe

- Every mutating tool call: resolve the capability and pass `execute(args, { idempotencyKey, timeoutMs, signal })` with a key derived from the business event (message id, not a timestamp). The adapter dispatch helpers attach none of those controls: good for reads, wrong for writes.
- `isError: true` is a connector outcome (the action failed), return its content to the model so it can retry or pick another tool. Reserve try/catch for thrown SDK errors.
- Server-side only: the key, `runtime_url` and any credential stay on the server; the browser talks to your routes, never to Vinkius.
- Optional metadata (`user.ensure({ kind: 'human', plan })`) is non-secret and powers your own segmentation; the SDK never reads back credential values, so a plan or locale field is exactly what metadata is for.

## Verify it works before shipping

Connect one user, load capabilities, assert the tool list matches that user's connectors; connect a second user to the same provider and assert the two lists are disjoint; retry one chat turn end to end and confirm no duplicate side effect thanks to the idempotency key.

Docs with the full walkthrough: https://vinkius.com/learn/connect-sdk/build-multi-user-chatbot
