Composio's default setup assumes one user. Here is how to scale it to hundreds of users each with their own connected accounts.
The Multi-Tenant Challenge
Most Composio tutorials show a single user authenticating a single app. In a real SaaS product, you have hundreds or thousands of users, each with their own connected accounts (their own Slack, their own Gmail, their own GitHub). Every agent action must use that specific user's credentials -- never another user's.
Composio handles this by scoping every call to a user_id, with each user's OAuth grants stored as connected accounts. This article explains that pattern and the production setup required to make it safe and scalable.
The user_id Pattern
In Composio v3, each user in your system is identified by a stable user_id that you choose. Every user has their own connected accounts -- one per authorised toolkit. When you fetch or execute a tool you pass that user_id, and Composio automatically uses that user's credentials.
from composio import Composio
composio = Composio(api_key="your-composio-api-key")
# Start a connection for a user (do this at sign-up or from a "Connect" button).
# user_id is your app's own user ID -- any stable string.
# auth_config_id (prefixed "ac_") is the reusable auth blueprint for the
# toolkit, created once in the Composio dashboard.
connection_request = composio.connected_accounts.initiate(
user_id="user-123",
auth_config_id="ac_your_slack_auth_config",
callback_url="https://yourapp.com/auth/callback?userId=user-123",
)
# Send this URL to your frontend for the user to authorise
print(f"Authorisation URL: {connection_request.redirect_url}")Using Per-User Credentials in Agent Workflows
from composio import Composio
from composio_langchain import LangchainProvider
from langgraph.prebuilt import create_react_agent
# One client, configured to return LangChain-compatible tools
composio = Composio(provider=LangchainProvider())
# Key: fetch tools scoped to a specific user via user_id
def get_tools_for_user(user_id: str):
return composio.tools.get(
user_id=user_id, # all tool calls use this user's credentials
tools=[
"SLACK_SEND_MESSAGE",
"GMAIL_SEND_EMAIL",
"GITHUB_CREATE_ISSUE",
],
)
# In your request handler:
async def handle_agent_request(user_id: str, user_message: str):
tools = get_tools_for_user(user_id) # scoped to this specific user
agent = create_react_agent(llm, tools)
result = await agent.ainvoke({"messages": [("user", user_message)]})
return resultNever reuse one user's tools for another. Always pass the requesting user's own user_id to composio.tools.get() and tools.execute(). Mixing up user_id values can result in one user's agent using another user's credentials.Checking What Apps a User Has Connected
from composio import Composio
composio = Composio(api_key="your-api-key")
# Map the toolkits your app supports to their auth config IDs (from the dashboard)
AUTH_CONFIGS = {
"slack": "ac_your_slack_auth_config",
"gmail": "ac_your_gmail_auth_config",
"github": "ac_your_github_auth_config",
}
def get_user_connections(user_id: str) -> list:
connected = []
for app, auth_config_id in AUTH_CONFIGS.items():
accounts = composio.connected_accounts.list(
user_ids=[user_id],
auth_config_ids=[auth_config_id],
)
for account in accounts.items:
connected.append({
"app": app,
"status": account.status, # ACTIVE, EXPIRED, FAILED, ...
"account_id": account.id,
})
return connected
# Use this to show users what they have connected in your settings UI
for conn in get_user_connections("user-123"):
print(f"{conn['app']}: {conn['status']}")Gating Agent Actions by Connected Apps
Before running an agent that needs a specific integration, verify the user has connected it. This prevents confusing errors and gives you the opportunity to prompt the user to connect the app first.
async def run_slack_agent(user_id: str, message: str) -> str:
# Check the user has an ACTIVE Slack connection before running
accounts = composio.connected_accounts.list(
user_ids=[user_id],
auth_config_ids=[AUTH_CONFIGS["slack"]],
)
statuses = [a.status for a in accounts.items]
if not statuses:
return "Please connect your Slack account first in Settings > Integrations."
if "ACTIVE" not in statuses:
return "Your Slack connection has expired. Please reconnect in Settings > Integrations."
# Safe to run -- user has an active Slack connection
tools = get_tools_for_user(user_id)
agent = create_react_agent(llm, tools)
result = await agent.ainvoke({"messages": [("user", message)]})
return result["messages"][-1].contentHandling Connection Expiry at Scale
OAuth tokens expire and sometimes get revoked by users on the third-party app's side. At scale, you will have users with expired connections running agent tasks. Build a proactive check into your scheduled jobs or agent middleware.
# Scheduled job: scan for expired connections and notify users
async def audit_expired_connections():
composio = Composio(api_key="your-api-key")
# Your own user IDs (paginate over your database in practice)
all_users = await db.get_all_user_ids()
for user_id in all_users:
# Fetch this user's connected accounts across all auth configs
accounts = composio.connected_accounts.list(user_ids=[user_id])
for account in accounts.items:
if account.status in ("EXPIRED", "FAILED"):
await notify_user_to_reconnect(
user_id=user_id,
account_id=account.id,
)Quick Reference
- Pass user_id on every composio.tools.get() and tools.execute() call -- always scope to the specific user
- Always pass the requesting user's own user_id -- never reuse another user's user_id or connected account
- Check connection status before running agent actions -- return a clear reconnect prompt if expired
- Initiate connected accounts proactively at user sign-up -- do not wait for the first agent action
- Run a scheduled audit job for expired connections and notify users proactively
- Store the connected account id returned from connected_accounts.initiate() in your database for direct reference