# Multi-tenant apps

By default, Swytchcode runs every call with **your** connected accounts. When you build an app whose own users each connect their Gmail, Slack or Stripe, every call has to run with **that user's** account instead.

Swytchcode calls these users **end users** (or tenants). You name each one with your own id for them, the **tenant id**, and pass it on every call made for them.

```text
Your app (logged-in user "alice")
      │  exec("gmail.user.profile.get", args, { tenantId: "alice" })
      ▼
Swytchcode picks alice's own Gmail credential
      │
      ▼
Gmail API (as alice)
```

A call made for an end user never falls back to your own account. If alice has not connected Gmail, the call fails with `tenant_not_connected` and nothing is sent.

---

## Before you begin

- The [Runtime SDK](/runtime-sdk/) on your server (JavaScript or Python), or the CLI.
- Your server signed in with a Swytchcode [API key](/cli/authentication/) (`SWYTCHCODE_TOKEN`) and linked to a [workspace](/guides/workspaces/).
- Each provider your end users connect set up once by you (step 1 below).

---

## 1. Set up each provider once

Run this yourself once per provider:

```bash
swy auth connect gmail
```

This is where you choose which OAuth app your end users sign in through: **Default Swytchcode** (ours) or **your own** developer app (Business and Enterprise). Your end users never see or change this choice. Until you have made it, connecting an end user to that provider fails with `setup_required`.

---

## 2. Connect an end user (OAuth)

Add one server route that creates a connect link for the logged-in user and returns it to the browser.

```js
// Node.js (Express)
const { connect } = require("@swytchcode/runtime");

app.post("/integrations/gmail/connect", requireLogin, (req, res) => {
  const { url } = connect({ provider: "gmail", tenantId: req.user.id });
  res.json({ url });
});
```

```python
# Python (FastAPI)
from swytchcode_runtime import connect

@app.post("/integrations/gmail/connect")
def connect_gmail(user = Depends(current_user)):
    return {"url": connect("gmail", user.id)["url"]}
```

In the browser, open the link in a popup directly from the user's click:

```js
button.onclick = async () => {
  const popup = window.open("", "connect", "width=520,height=720");
  const { url } = await (await fetch("/integrations/gmail/connect", { method: "POST" })).json();
  popup.location.href = url;
};
```

Opening the popup inside the click (and loading the link afterwards) keeps browsers from blocking it. The end user signs in to Google and closes the window; the connection is ready.

Each link works **once** and **expires after 10 minutes**, so create a new one on every click. A reopened or forwarded link is refused.

### Bots and mobile apps

There is no browser click to open a popup from, so hand the end user the link itself. Create it on your server only when they ask to connect, from the identity your bot or app already trusts:

- **Chat bots (Slack, Telegram, WhatsApp, Discord):** send the link in a direct message to that user only, never in a shared channel or group, where anyone could open it first. Take the tenant id from the platform's verified sender id, not from text the user typed.
- **Mobile apps:** have your backend create the link for the signed-in user and open it in the system's in-app browser (`ASWebAuthenticationSession` on iOS, Custom Tabs on Android), not an embedded web view: Google and other providers refuse sign-in inside web views.

The same rules apply: one link per request, it works once, and it expires after 10 minutes. When the user finishes, the page tells them they can close it. Your bot or app learns the account is ready from the next call made for them, which no longer fails with `tenant_not_connected`, or from `swy auth status --tenant <id>`.

---

## 3. Run calls for the end user

Pass the tenant id on every call made for that user:

```js
const { exec } = require("@swytchcode/runtime");

const profile = await exec("gmail.user.profile.get", { params: { userId: "me" } }, { tenantId: req.user.id });
```

```python
from swytchcode_runtime import exec

profile = exec("gmail.user.profile.get", {"params": {"userId": "me"}}, tenant_id=user.id)
```

```bash
swy exec gmail.user.profile.get --tenant alice
```

The first call for an end user fetches their credential and caches it on your server; later calls use the cache and refresh it when needed.

If your [policies](#policies) ask for human approval, also say who the end user is, so the approver knows whose account the call runs on:

```js
await exec("stripe.refund.create", args, { tenantId: req.user.id, tenantLabel: `${req.user.name} (${req.user.email})` });
```

```python
exec("stripe.refund.create", args, tenant_id=user.id, tenant_label=f"{user.name} ({user.email})")
```

```bash
swy exec stripe.refund.create --tenant alice --tenant-label "Alice Smith (alice@acme.com)"
```

The label only reaches the approval message. Without it, the approver sees the tenant id alone.

---

## API-key providers

For providers that use API keys (Stripe, OpenAI, ...), collect the key in your own form, send it to your server, and save it there:

```js
const { saveKey } = require("@swytchcode/runtime");

saveKey({ provider: "stripe", tenantId: req.user.id, key: req.body.stripeKey });
```

```python
from swytchcode_runtime import save_key

save_key("stripe", user.id, form.stripe_key)
```

The key is stored encrypted on your server only and is never sent to Swytchcode. Swytchcode records only that this end user has a key, so they count toward your plan's end-user limit; saving a key therefore needs your API key and a connection to Swytchcode.

---

## Disconnect an end user

When a user disconnects a provider or deletes their account:

```js
const { disconnect } = require("@swytchcode/runtime");

disconnect({ provider: "gmail", tenantId: req.user.id });
```

```python
from swytchcode_runtime import disconnect

disconnect("gmail", user.id)
```

---

## AI agents

When an AI agent picks the tools, bind the client to the end user so every tool it runs uses that user's accounts. Create one client per request, from your server's session:

```js

const client = new Swytchcode(provider, { tenantId: req.user.id, tenantLabel: req.user.email });
const tools = await client.tools.get({ toolkits: ["gmail"] });
// ... pass tools to your agent framework; every call runs for req.user.id
```

```python
from swytchcode_runtime import Swytchcode

client = Swytchcode(provider, tenant_id=user.id, tenant_label=user.email)
tools = client.tools.get(toolkits=["gmail"])
```

This covers `client.tools.get()` with any framework (OpenAI Agents, Vercel AI, LangGraph, CrewAI) and `handleToolCalls` / `handle_tool_calls`.

---

## Policies

[Policies](/configuration/policy-json/) apply to calls made for end users like any other call, with these rules:

- **What a policy remembers is kept per end user.** `called_before`, `returned_by` and `repeat` only see the same end user's earlier calls: a charge listed for alice never allows bob to refund it, and bob sending what alice sent is not a repeat.
- **Approvals are per end user.** An approval (and its `reuse` window) for alice's call never covers bob's.
- **Volume limits are shared unless you say otherwise.** `rate` and `sum` count every end user together, which protects your provider quota. Add `"per": ["$tenant"]` to give each end user their own limit:

```json
{
  "id": "emails-per-user",
  "target": ["gmail.send"],
  "when": { "operator": "rate", "window": "1h", "max": 20, "per": ["$tenant"] },
  "action": { "type": "RATE_LIMITED", "message": "{{$tenant}} has sent {{$count}} emails this hour" }
}
```

- **`$tenant` works in any condition**, for example `{"field": "$tenant", "operator": "in", "value": ["trial-1", "trial-2"]}`.
- **Policy alerts** go out on your own account, never the end user's.

---

## Rules that keep end users apart

- **Take the tenant id from your server's session.** Never from the browser, a request body, a query string or a header the client controls: anyone could send another user's id.
- **Never call without the tenant id for an end user.** A call without one runs on your own account.
- **Create connect links on the server** and hand them only to the user they were made for.
- **Keep the provider choice to yourself.** It is made once with `swy auth connect <provider>`.

---

## Errors

| Category | Exit code | Meaning | What to do |
|---|---|---|---|
| `tenant_not_connected` | 3 | This end user has not connected the provider. | Show them your Connect button. Do not retry. |
| `setup_required` | 3 | You have not set the provider up yet, or it needs your own app and you have not registered one. | Run `swy auth connect <provider>` once. |
| `plan_limit` | 3 | Your plan does not allow this: the end-user limit is reached, or the provider needs your own app. | Upgrade (the error's `docs_url` links to it). |

In the SDKs the category is on `error.details.category` (JavaScript) or `error.details["category"]` (Python).

---

## Plans and limits

- **End users per plan:** Developer 10, Pro 1,000, Business 10,000, Enterprise custom.
- **An end user is a tenant id within one workspace.** alice connecting Gmail and Google Calendar in one workspace counts once; the same id in another workspace (another project) counts again. OAuth connections and saved API keys count the same way.
- **Reconnecting** an end user who already counts is always allowed, even at the limit.
- **Executions** made for end users count toward your monthly execution limit like any other, including those made with an API key.
- **Providers that charge per API call** (for example X) run only on your own developer app, available on Business and Enterprise.

---

## Audit and approvals

- Every local audit entry records the tenant id and the connected account used (never the credential). Filter with `swy audit stats --tenant alice` (also `network` and `policy`).
- Cloud audit logs carry `tenant_id` and `connected_account_uuid`.
- [Human approvals](/policies/human-approval/) are per end user: an approval for alice's call never covers bob's, and the approved call runs again for alice only.
- The approver's Slack or Telegram message says who asked and for whom:

  ```text
  Approval Required
  Refund of 750 for alice needs approval
  Requested by: Jane Doe (Acme Inc, jane@acme.com)
  Workspace: blue-otter-4f2a
  End user: Alice Smith (alice@acme.com), id alice
  Policy: big-refund
  ```

  The first line is the policy's `message`, with its placeholders filled in (without one: `<policy id> - <tool>`). "Requested by" is your Swytchcode account and workspace. "End user" is the `tenantLabel` you passed, with the tenant id; without a label it shows the id alone. Calls on your own account have no "End user" line.

---

## CLI equivalents

| SDK | CLI |
|---|---|
| `connect({ provider, tenantId })` | `swy auth connect <provider> --tenant <id> --json` |
| `saveKey({ provider, tenantId, key })` | `swy auth connect <provider> --tenant <id>` with the key on stdin |
| `disconnect({ provider, tenantId })` | `swy auth disconnect <provider> --tenant <id>` |
| `exec(id, args, { tenantId })` | `swy exec <id> --tenant <id>` |
| `exec(id, args, { tenantId, tenantLabel })` | `swy exec <id> --tenant <id> --tenant-label "<label>"` |
| - | `swy auth status --tenant <id>` lists one end user's accounts |
