Multi-Tenant OAuth for AI Agents: How to Store a Credential for Every Customer
Multi-tenant OAuth means one stored grant per customer, not one token in a .env file. If every customer connects their own Slack or GitHub, you need a hosted connect flow, a credential stored against their ID, and every call tagged with whose account it used. Here is who does what.
Key takeaways
- -A token in .env is a single-tenant design. Multi-tenant means one stored grant per customer, refreshed without the model ever seeing it.
- -Nango, Composio, Arcade and Pipedream Connect all host the connect screen and store the grant. Pick by whether you want to own the integration code.
- -Swytchcode does not host an end-user connect screen. Its runtime executes per-tenant calls using grants your own service holds.
- -The leak that bites is not a stolen token, it is a missing tenant check: customer A's agent run reaching customer B's workspace.
- -Tag every call with the customer ID at the runtime, not in the prompt, and keep denied calls in the same log as allowed ones.
Multi-tenant OAuth is the problem of letting every customer of your product connect their own Slack workspace or GitHub organisation, which means storing one credential per customer and reaching for the right one on every call. A single token in a .env file serves exactly one tenant, and the day you sign your second customer it becomes a bug.
Nango, Composio, Arcade and Pipedream Connect all do this job: they host the connect screen, keep a grant per end user, and refresh it without your code touching the secret. Swytchcode does not replace them. It runs the API call after the grant exists, which is a different part of the same pipeline.

The mistake that looks like it works
Almost every multi-tenant agent starts single-tenant by accident. The first version reads SLACK_BOT_TOKEN from the environment, posts a message, and works perfectly in the demo. It works because there is one customer and that customer is you.
Then a second customer arrives and the questions start. Whose token is this? Where does the second one go? If you add SLACK_BOT_TOKEN_ACME, what happens at customer fifty? Who refreshes them when they expire? And the one that actually hurts: when the agent runs a background job at 2am, how does it know which customer this run belongs to?
The failure here is almost never a stolen token. It is a missing tenant check. An agent holding valid credentials for forty workspaces, with nothing in the call path asserting which workspace this particular run may touch, is one bad loop variable away from posting one customer's data into another customer's channel. That is not a hypothetical class of bug, it is the most common serious one in multi-tenant agent code.
What "per customer" actually requires
Four things, and all four have to be true at once.
A connect flow the customer completes themselves. Not a token an engineer pastes into a config file on their behalf. The customer clicks Connect, sees their own provider's consent screen, and approves the scopes. You never hold their password and they can revoke you.
A grant stored against that customer's ID. Access token, refresh token, scopes and expiry, keyed by your internal customer identifier. When it expires, something refreshes it in the background without the model, the prompt or your application code seeing the secret.
Every later call tagged with that customer. The tenant ID travels with the call as a parameter of the execution, not as a sentence in a prompt. A model can be talked out of a sentence in a prompt. It cannot talk its way past a credential lookup that takes the tenant ID as an argument.
Scopes narrow enough to be boring. If your agent reads channel history, ask for history. Do not ask for chat:write because you might want it later. Customers read these screens, and security teams read them twice.
Which platform manages the grants
All four of these will store a credential per customer. They differ on how much of the integration you own and how they bill.
Nango
The one to pick when the integration code should live in your repository. Embedded OAuth for more than 1,000 APIs, around 7,000 prebuilt tools, triggers and syncs, real-time webhooks and SOC 2 Type II. You can read and edit the integration, which matters when a provider does something strange and you need to fix it this afternoon rather than file a ticket.
Free covers 10 connections, 10 compute hours and 10GB of transfer a month, hard-capped and reset monthly. Pay-as-you-go is $50 a month including $50 of credits, then $0.29 per connection, $0.72 per compute hour and $0.50 per GB. Their FAQ says per-connection pricing can drop to $0.01 at Enterprise scale, which is the number that matters if you are planning for thousands of customers. Enterprise adds self-hosting, SAML SSO, SCIM, RBAC and HIPAA with a BAA.

Composio
The one to pick when you want the catalog and the connect link hosted, and you would rather not maintain integration code at all. More than 1,500 toolkits, Python and TypeScript SDKs, plus MCP.
The free Hobby plan is generous for this use case specifically: unlimited connected accounts, plus 100,000 tool calls and 50,000 trigger events a month, and three team members. Connected accounts not being metered is unusual and it makes Composio cheap to grow into. Pro is $29 a month with a $29 usage credit, unlimited team members and spend controls. White-labelling the connect screen properly is an add-on at $0.30 one-time per connection. SSO and SCIM are Enterprise only.

Arcade
Per-user tool calls over MCP and SDKs, and the only one of the four that meters authorizations as a first-class unit. Free is 2,000 tool calls and 2,000 auth events a month. Team is $25 a month plus $0.01 per tool call and $0.10 per auth event.
That auth-event rate is worth modelling before you commit. If your product re-authorizes customers often, or you have a lot of customers each making few calls, auth events can be the larger line on the bill. Enterprise can run in your VPC or fully air-gapped.
Pipedream Connect
The widest catalog of the four, with over 3,000 connectors. The detail that catches people: the free plan is development only. There is no production environment on it, so you cannot ship per-customer connections without paying.
Startup is $150 a month billed monthly or $99 billed annually, and includes 100 external users, then $2 per user per month. An "external user" is anyone who has connected at least one account through Connect, identified by the external user ID you pass. Usage runs on credits: 10,000 included, then $0.012 each, where one credit is 30 seconds of compute. SSO, SCIM and HIPAA are Business tier.
A quick way to choose
| If this is your situation | Start with |
|---|---|
| You want to own and edit the integration code | Nango |
| You want zero integration code and a hosted connect button | Composio |
| You have many customers and few calls each | Nango or Composio, and model Arcade's auth-event rate first |
| You have few customers and very many calls each | Compare Composio's included allowance against Arcade and Pipedream per-unit rates |
| You need it in your own VPC or air-gapped | Nango Enterprise or Arcade Enterprise |
| Your customer count is heading for thousands | Nango Enterprise, for the per-connection floor |
The fuller landscape, including what sits outside this specific job, is in best tools for AI agents to call APIs. If you are specifically evaluating a move off one of these, we keep Nango alternatives, Composio alternatives, Arcade alternatives and Pipedream alternatives.
Where Swytchcode fits, and where it does not
We should be direct about this because it is the thing people get wrong when they first read our site.
Swytchcode does not host a "Connect your Slack" screen for your end users. If you need ten thousand customers to each click through a Slack consent flow, that grant collection is not our job and we are not going to pretend otherwise. Use Nango or Composio for that screen.
What the runtime does is execute the call once a grant exists, using credentials your own service holds. For multi-tenant work the SDK takes the tenant explicitly:
import { Swytchcode } from "@swytchcode/runtime";
// one client per tenant, credentials resolved for that tenant only
const sc = new Swytchcode("slack", {
tenantId: customer.id,
tenantLabel: customer.name,
});
const res = await sc.tools.execute("slack_web.chat.postmessage.create", {
body: { channel: customer.alertChannel, text: "Nightly sync finished." },
});The tenant ID is an argument, not a prompt instruction. The credential lookup, the policy evaluation and the audit record all key off it. That is the structural answer to the cross-tenant bug described earlier: there is no code path where the runtime sends a call without knowing whose account it is using.
For the single-tenant case, where your company owns one account per provider, the simpler standalone call is enough and there is no .env file involved at all:
swy get slack
swy add method slack_web.chat.postmessage.create
swy auth connect slackimport { exec } from "@swytchcode/runtime";
await exec("slack_web.chat.postmessage.create", {
body: { channel: "C01234567", text: "Deploy finished." },
});Credentials sit in ~/.swytchcode/credentials.db outside your project, and the mode is set once by swy init --mode=production in .swytchcode/tooling.json. The model never sees a key, which is the whole argument in credential brokering for AI agents.
The parts a grant does not cover
Per-user OAuth is a real and necessary control. It stops customer A reading customer B's inbox. It is worth being clear about what it does not stop, because teams routinely assume it covers more than it does.
A valid grant from customer A does not stop your agent deleting all of customer A's channels. The grant says the agent may act as that user; it says nothing about how much damage that user is allowed to do. That is a policy question, and it belongs in version control rather than in a prompt. See policy-as-code for AI agents.
A valid grant also does not stop a retry duplicating a write. If a Slack post times out and your framework retries it, the grant is still perfectly valid for both attempts and the customer sees the message twice. Idempotency explained covers the fix.
And a grant does not give you a record. When a customer asks why their agent posted something, you need a log written by the thing that sent the request, carrying the tenant ID, the operation, the policy decision and the outcome. Logging every API call an agent makes goes through the fields and the ones to leave out.
One last thing that is specific to multi-tenant setups: when a provider changes its API, you find out across every customer at once. Every API added to Swytchcode becomes agent-ready in minutes and is then tracked for breaking changes by observing its specs and documentation, which is how a renamed parameter becomes an integration update instead of forty simultaneous support tickets.
FAQ
Which platforms manage per-customer Slack and GitHub credentials?
Nango, Composio, Arcade and Pipedream Connect all host a connect flow and store one grant per end user. Pick Nango if you want the integration code in your own repository, Composio if you want the hosted catalog and connect link with no code to maintain, Arcade if per-user tool calls over MCP suit your architecture, and Pipedream if you need the widest connector catalog. Swytchcode does not store an end-user Slack grant for you; it executes the call once the grant exists.
Can I just store the tokens myself in my own database?
You can, and some teams should. Encrypt them at rest, key them by your internal customer ID, handle refresh in a background job, and never let the token reach a prompt or a model response. The work you are taking on is each provider's OAuth quirks, token rotation, revocation handling and scope changes, which is exactly what the four platforms above sell. If you have two providers, rolling it yourself is reasonable. At ten, it stops being reasonable.
How do I stop one customer's agent run from touching another customer's account?
Make the tenant ID an argument to the call rather than an instruction in a prompt. The credential lookup, the policy check and the log entry should all key off the same identifier, so there is no code path that sends a request without knowing whose account it uses. Then test it: write a case that asks for customer B's channel while running as customer A, and assert it fails.
Is Nango or Composio cheaper for per-customer credentials?
It depends on what you are counting. Composio does not meter connected accounts at all, which makes it very cheap if you have many customers each making a modest number of calls. Nango charges $0.29 per connection on pay-as-you-go but says that drops toward $0.01 at Enterprise scale, which is better if you have a very large customer count. Arcade's $0.10 per auth event is the one to model carefully if customers re-authorize often. Price your own expected shape on both pages; the headline tier prices will mislead you.
Does Swytchcode support multi-tenant credentials at all?
Yes, on the execution side. The Swytchcode client takes a tenantId and tenantLabel, and resolves credentials, policies and audit records for that tenant only. What it does not do is collect the grant from your end user in the first place. That connect screen comes from Nango, Composio, Arcade or Pipedream Connect, or from your own OAuth app.
What should never go in the per-customer credential path?
The refresh token should never appear in a prompt, a model response, a log line or an error message. Neither should the Authorization header. If your audit log would let someone replay a customer's calls, the log has become a second copy of the credential store, which is a worse problem than the one you were solving
More content
How to Log Every API Call an AI Agent Makes
Log the call where it is sent, not where it is described. Record the operation, the actor, the idempotency key, the policy decision and the outcome. Never record the credential. If the agent writes its own log, the calls that were blocked quietly disappear.
How to Connect GitHub to an AI Agent (2026)
Most of what you want an agent to do on GitHub needs no push access at all. Working that out first is the difference between a useful teammate and a token that can rewrite your main branch.
Composio Pricing Compared with Nango, Arcade, Pipedream, and Swytchcode
Composio is free for 100,000 tool calls a month, then $29. Nango is $50 pay-as-you-go. Arcade Team is $25 plus a cent a call. Pipedream Startup is $99 billed annually. Swytchcode is $149 for a million. The headline prices are the least useful part, so here is the arithmetic.
