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
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:
- Adds
tenant_id = 'user_abc123'to everyINSERT— all data written is tagged - Adds
WHERE tenant_id = 'user_abc123'to everySELECT— you only ever read your own data - Retrieves credentials scoped to that tenant — API calls use that user's token, not yours
- Routes incoming webhooks to the correct tenant via
?tenantId=user_abc123in 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:
// 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_abc123Corsair reads tenantId from the query string and automatically scopes the database write to that user. No routing logic on your end.