Files
composio-skills-reference/composio-sdk/rules/app-user-context.md
sohamganatra b8b711dff6 Add Composio SDK skill with rules and agent config
Adds composio-sdk/ with SKILL.md, AGENTS.md, and 18 rule files
covering Tool Router, direct execution, triggers, and auth patterns.

Source: composiohq/skills

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 22:54:21 -08:00

5.6 KiB

title, impact, description, tags
title impact description tags
User Context and ID Patterns for Applications HIGH Critical patterns for user identification, multi-tenancy, and data isolation in production applications
user-context
security
multi-tenancy
isolation
production

User Context and ID Patterns

Every Composio operation requires a userId parameter for security and data isolation. Users can only access their own connected accounts.

The 'default' User ID

default refers to your project's default account.

Only use 'default' for:

  • Testing and development
  • Single-user applications
  • Internal tools with no external users

Never use in production multi-user apps - it bypasses user isolation.

Production User ID Patterns

Use your database's primary key:

const userId = user.id; // "550e8400-e29b-41d4-a716-446655440000"

await composio.tools.execute('GITHUB_GET_REPO', {
  userId: userId,
  arguments: { owner: 'example', repo: 'repo' },
});

Pros: Stable, immutable, already exists, no mapping needed

External Auth ID (Acceptable)

Use IDs from Auth0, Firebase, etc:

const userId = user.externalId; // "auth0|507f1f77bcf86cd799439011"
// Or with prefix
const userId = `user_${user.id}`; // "user_12345"

Pros: Works with external auth, human-readable, allows namespacing Cons: May require mapping, usernames can change

const userId = user.email; // "user@example.com"

Only use when:

  • Email is guaranteed immutable
  • No other unique identifier available
  • SSO requires email-based identification

Cons: Emails can change, privacy concerns

Organization-Based Applications

For team/org-wide tool access, use organization ID as userId:

// All users in org share same connected accounts
const userId = organization.id; // "org_550e8400..."

await composio.tools.execute('SLACK_SEND_MESSAGE', {
  userId: userId, // organization ID, not individual user
  arguments: { channel: '#general', text: 'Team message' },
});

Use organization IDs when:

  • Team/org tools (Slack, MS Teams, Jira)
  • Enterprise apps with IT admin connections
  • Shared resources across users
  • Role-based access at org level

Example:

// Admin connects Slack for entire org
async function connectOrgToSlack(orgId: string) {
  const request = await composio.connectedAccounts.link(orgId, 'slack');
  return request.redirectUrl;
}

// Any user in org can use connected tools
async function sendMessage(orgId: string, message: string) {
  return await composio.tools.execute('SLACK_SEND_MESSAGE', {
    userId: orgId,
    arguments: { channel: '#general', text: message },
  });
}

// Check org connections
async function listOrgConnections(orgId: string) {
  return await composio.connectedAccounts.list({
    userIds: [orgId],
  });
}

Shared vs. Isolated Connections

Isolated (User-Level)

Each user has their own connections:

await composio.connectedAccounts.link('user_123', 'github_config');
await composio.connectedAccounts.link('user_456', 'github_config');

// Each execution uses that user's account
await composio.tools.execute('GITHUB_GET_REPO', {
  userId: 'user_123', // Uses user_123's GitHub
  arguments: { ... },
});

Use for: Personal integrations, individual credentials, privacy-critical

Shared (Organization-Level)

All users share organization connections:

await composio.connectedAccounts.link('org_acme', 'github_config');

// All org users use same connection
await composio.tools.execute('GITHUB_GET_REPO', {
  userId: 'org_acme', // All users share
  arguments: { ... },
});

Use for: Org-wide access, centralized credentials, simplified administration

Security Best Practices

Never Expose User IDs to Frontend

// ❌ DON'T: Allow frontend to specify userId
app.post('/execute-tool', async (req, res) => {
  await composio.tools.execute(req.body.tool, {
    userId: req.body.userId, // SECURITY RISK
    arguments: req.body.arguments,
  });
});

// ✅ DO: Derive userId from authenticated session
app.post('/execute-tool', async (req, res) => {
  const userId = req.user.id; // From auth session
  await composio.tools.execute(req.body.tool, {
    userId: userId,
    arguments: req.body.arguments,
  });
});

Validate User Ownership

async function executeForUser(authenticatedUserId, targetUserId, tool, args) {
  if (authenticatedUserId !== targetUserId) {
    throw new Error('Unauthorized');
  }
  return await composio.tools.execute(tool, {
    userId: targetUserId,
    arguments: args,
  });
}

Common Patterns

Express Middleware

app.use((req, res, next) => {
  req.userId = req.user.id; // From authenticated session
  next();
});

app.post('/execute-tool', async (req, res) => {
  const result = await composio.tools.execute(req.body.tool, {
    userId: req.userId,
    arguments: req.body.arguments,
  });
  res.json(result);
});

Debug User Context

const accounts = await composio.connectedAccounts.list({
  userIds: [userId],
});

console.log(`User ${userId} has ${accounts.items.length} accounts`);
accounts.items.forEach(account => {
  console.log(`- ${account.toolkit.slug}: ${account.status}`);
});

Key Points

  • Use database UUIDs - Most stable and reliable
  • Never expose userId - Always derive from authenticated session
  • Validate ownership - Ensure users only access their data
  • Use consistent format - Pick one pattern and stick to it
  • Organization IDs - For team-wide tool access
  • Handle changes gracefully - Maintain mapping if IDs can change