Corsair
Concepts

Multi-Tenancy

Building multi-tenant applications with Corsair

Multi-tenancy means each user of your app connects their own accounts. User A connects their GitHub. User B connects theirs. They never see each other's data.

Enable it with one flag. Then scope every operation to a user with withTenant().


Enable it

corsair.ts
export const corsair = createCorsair({
    multiTenancy: true,   // ← this is all it takes
    plugins: [github(), slack()],
    database: new Pool({ connectionString: process.env.DATABASE_URL }),
    kek: process.env.CORSAIR_KEK!,
});

With multiTenancy: true, Corsair won't let you call plugins directly — every operation must go through withTenant(). This is enforced at the type level, so you'll get a compile error if you forget.


Use withTenant()

Pass any stable user identifier — database ID, auth provider ID, anything:

const tenant = corsair.withTenant('user_abc123');

// API calls use that user's credentials
await tenant.github.api.repositories.list({ type: 'owner' });

// Database queries return only that user's data
const repos = await tenant.github.db.repositories.findAll();

// Credentials are stored per-user
await tenant.github.keys.set_api_key(userGithubToken);

Every read, write, and API call is automatically scoped. There is no way to accidentally query another user's data through the normal API.


How Corsair scopes it

When you call withTenant('user_abc123'), Corsair:

  1. Adds tenant_id = 'user_abc123' to every INSERT — all data written is tagged
  2. Adds WHERE tenant_id = 'user_abc123' to every SELECT — you only ever read your own data
  3. Retrieves credentials scoped to that tenant — API calls use that user's token, not yours
  4. Routes incoming webhooks to the correct tenant via ?tenantId=user_abc123 in the URL

No middleware, no manual filtering. Corsair handles it inside the database adapter.


Per-user credential setup

Each user connects their own accounts. Store credentials when they authenticate:

auth-callback.ts
// Called after GitHub OAuth or when user pastes their token
export async function saveUserCredentials(userId: string, githubToken: string) {
    const tenant = corsair.withTenant(userId);
    await tenant.github.keys.set_api_key(githubToken);
}

Credentials are encrypted with your KEK and stored per-tenant. One user's token can never be decrypted by another tenant's context.


Webhooks

When registering webhooks, include the user's ID in the URL:

https://your-app.com/api/webhook?tenantId=user_abc123

Corsair reads tenantId from the query string and automatically scopes the database write to that user. No routing logic on your end.


What's next