One Runtime, Many Tenants
Corsair’s multi-tenant design answers a specific product problem: one application may connect many users to the same providers, but each user must receive different credentials and different synchronized data. The focus here is how withTenant() carries that identity through the runtime, how account rows are provisioned, and where the application must still authenticate the caller. The result is a tenant-scoped call path that is safer precisely because it does not pretend that scope and identity are the same thing.
withTenant() makes scope part of the client
In a single-tenant application, a feature can call a provider namespace directly because the application has one default connection. A multi-tenant Corsair instance returns a different root shape. The root exposes withTenant(id), and the plugin APIs, database clients, and account keys are reached through the client produced for that tenant. At the type level, the multi-tenant wrapper does not expose direct plugin operations, so forgetting the scope becomes a compile-time problem rather than merely a convention written in a code review.
At runtime, the scope does more than prefix a query. When the application calls corsair.withTenant('workspace-a'), the resulting client carries an effective tenant identifier into each plugin’s account lookup and entity client. The API operation resolves the account for that tenant and plugin, the key manager retrieves that account’s encrypted credential, and local entity queries constrain themselves to the account that owns the data. Hooks receive the tenant context as well, so application logic can make a downstream action with the same identity instead of falling back to a global client by accident.
The scope is applied at several layers because no single filter is enough. A database query restricted to a tenant but paired with a global API key can still act on the wrong external account. A correctly selected token paired with an unscoped local query can still display another user’s entity. A webhook hook that writes to an application table without carrying its tenant can detach a provider event from the user who owns it. Corsair’s value is that the same scope is available to account lookup, credentials, entities, hooks, approval records, and workflow clients; the application must preserve that scope whenever it leaves the bound request path.
A tenant ID is also an application identity, not a provider identity. The application may choose a database user ID, organization ID, or workspace slug, while GitHub or Slack uses an installation, organization, or team identifier of its own. The account row is the bridge between those namespaces. Confusing them can produce plausible but incorrect routing: a provider identifier might locate an external account, but it does not by itself say which application user is authorized to use it. The bridge has to be established during connection and then checked when events and requests arrive.
The contrast between two users makes the rule concrete. If one person connects GitHub and another connects GitHub separately, both connections use the same plugin contract but not the same account row or credential. A repository list made through the first tenant must use the first tenant’s GitHub account; a local issue search must return only entities associated with that account. The provider name is shared. The authority to act and the data that can be read are not.