AI Connect/Integration/Multi-tenant patterns
Multi-tenant patterns
One application-scoped client, scope resolved before request input, capabilities returned only for the authenticated user, and an isolation boundary you can test.
A single Vinkius client serves every user of your application. Isolation comes from how you derive the externalId, not from client instances. This page is the security checklist for wiring the SDK into a multi-tenant backend.
Vinkius never meets your real users. The platform knows your Application and the opaque identifier your backend passes, nothing else: no emails, no names, no cross-application profile. Thousands of users under one Application, each with their own connectors, credentials and state, while your user base stays entirely on your side.
Create one application-scoped client
// lib/vinkius.ts — server only
import { Vinkius } from '@vinkius/connect';
export const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!,
apiKey: process.env.VINKIUS_APP_KEY!,
});The client holds application credentials, never per-user state. Do not build one client per user; the user scope travels in the route.
Resolve scope before reading request input
Bind the externalId to your authenticated session before anything else. The authorization boundary is your session lookup; an App ID and key authorize the application, never a browser user:
async function handleCapabilities(request: Request) {
const session = await requireSession(request); // your auth code
const capabilities = await vinkius.user(session.userId).capabilities();
// ...
}Return only the authenticated user's capabilities
Project the fields your client needs and nothing else. The capability objects are already scoped to the user the session resolved:
return Response.json(
capabilities.map(({ name, description, inputSchema }) => ({
name,
description,
inputSchema,
})),
);Authorize connector setup separately
Writing credentials is a privileged operation. Verify that the session is allowed to administer the connector for that user before calling connect() and credentials.set():
const github = vinkius.user(session.userId).connector('github');
await github.connect();
await github.credentials.set({ GITHUB_TOKEN: submittedToken });Execute only capabilities loaded in the same scope
Capabilities carry the connection route that produced them. Never accept a capability name from the client and execute it against a handle resolved from a different scope; load the capability set inside the authenticated request and dispatch from it:
const capabilities = await vinkius.user(session.userId).capabilities();
const capability = capabilities.findCapability(requestedName);
if (!capability) {
return Response.json({ error: 'not available for this user' }, { status: 404 });
}
const result = await capability.execute(args, { idempotencyKey });Use metadata only for non-secret application context
user.ensure(metadata) upserts the user and stores application context such as plan tier or labels. It is not a credential store; keep secrets in connector credentials, which are write-only.
Test the isolation boundary
Two users connecting the same connector must never see each other's state. A minimal test shape:
const alice = vinkius.user('alice_123');
const bob = vinkius.user('bob_456');
await alice.connector('github').connect();
const bobConnectors = await bob.connectors();
// bob's list must not contain alice's connectionRun this against a staging environment with its own App ID and key. See Authentication and scope for the environment separation rules.
