AI Connect/How to create/Un SaaS d'IA multi-tenant

Un SaaS d'IA multi-tenant

Demandez à l’IA à propos de Vinkius

Déployez l'IA avec des connecteurs auprès de chaque client de votre plateforme B2B : délimitez chaque external_id par tenant et utilisateur, maintenez chaque organisation isolée sur une seule clé d'application et ajoutez un contrôle par tenant sans jamais exposer un identifiant au-delà de la frontière. Vos clients reçoivent une plateforme de connectivité ; vous ne leur livrez jamais la plomberie.

Livrez le SaaS IA que votre catégorie attendait : chaque client (tenant) reçoit une équipe d'utilisateurs, chaque utilisateur reçoit son IA accédant à son GitHub, Jira et Slack, chaque tenant isolé sur votre unique Application key, et chaque utilisateur de chaque tenant adossé à des milliers de connexions IA dès le premier jour. Aucune intégration construite par vous, aucun jeton stocké par vous, aucune identité quittant votre base de données.

Chaque plateforme d'intégration du marché vous dira que ce problème se termine par un contrat par tenant, une facture par tenant, ou des mois de vos ingénieurs construisant l'isolation à la main. La réponse de l'AI Connect SDK est celle qu'aucun concurrent ne peut copier sans réarchitecturer son produit : livrez des fonctionnalités d'IA, pas des projets d'intégration. Ce guide vous montre comment faire partager une unique clé d'application à tout un marketplace de tenants sans jamais franchir la moindre frontière.

Ici, l'« utilisateur » est une personne au sein de votre client, l'external_id doit donc porter à la fois le tenant et l'humain. Cette décision unique constitue le modèle d'isolation. Quand cela fonctionne, votre produit fait ce qu'il prend normalement des années à une entreprise de plateforme et à une équipe sécurité pour promettre : chaque organisation est une île, chaque utilisateur est citoyen d'exactement une île, et vous administrez tout l'archipel depuis une seule clé. Vinkius ne rencontre jamais vos utilisateurs réels. Leurs e-mails, leurs noms, leurs profils ne quittent jamais votre base ; la plateforme ne voit que l'id opaque que votre backend lui transmet.

Vinkiusnever sees your usersYour application keyvk_app_*acmetenant · isolatedusersown toolsglobextenant · isolatedusersown tools404404initechtenant · isolatedusersown tools
Every tenant an island: users are citizens of exactly one island, a cross-tenant attempt is a 404, and your brand faces every customer while Vinkius stays invisible.

Le contrat d'isolation

  • Une Application = votre produit. Tous les tenants partagent normalement votre unique appId.
  • L'external_id encode l'adresse. cus_<tenant>_u_<user> est la frontière ; les capacités ne se résolvent qu'en son sein.
  • Le croisement de tenants est un 404, pas un 500. Un id exposé ou mal routé ne peut pas lire la connexion d'un autre tenant, Vinkius le rejette comme hors périmètre.
  • Vos utilisateurs restent sous votre contrôle. Aucun e-mail ni profil n'atteint Vinkius ; vous ne lui remettez qu'un id opaque. Votre relation client demeure sous votre contrôle.

L'isolation découle d'un external_id bien formé ; traitez-le donc comme une entrée critique pour la sécurité. Construisez-le toujours à partir de claims de tenant et d'utilisateur authentifiés, jamais à partir de données brutes de la requête, et ne laissez jamais un tenant fournir l'id d'un autre. Pour le modèle détaillé des garanties, voir Authentification et périmètre et Sécurité.

1. Adresser un utilisateur à l'intérieur d'un tenant

Composez un id déterministe et sûr pour les URL à partir des deux ids auxquels votre authentification se fie déjà.

typescript
interface AuthedPrincipal { tenantId: string; userId: string } // de votre JWT/session

const externalIdFor = (p: AuthedPrincipal) =>
  `cus_${p.tenantId}_u_${p.userId}`; // "cus_acme_u_9f2c"

2. Un seul client partagé

typescript
// server/vinkius.ts
import { Vinkius } from '@vinkius/connect';

export const vinkius = new Vinkius({
  appId: process.env.VINKIUS_APP_ID!,
  apiKey: process.env.VINKIUS_APP_KEY!,
  maxRetries: 2,
});

3. Résoudre l'acteur depuis une session vérifiée

Dérivez le principal depuis votre jeton, puis transmettez-le au SDK. Tout l'aval est délimité, car externalIdFor l'est.

typescript
import { vinkius } from './vinkius';

export async function actorFor(req: Request) {
  const claims = await verifySession(req); // votre authentification/autorisation
  if (!claims) throw new Error('unauthenticated');
  return vinkius.user(externalIdFor(claims));
}

4. Laisser chaque utilisateur connecter ses propres outils

Deux personnes dans deux entreprises connectent la même intégration GitHub et obtiennent deux connexions, deux jeux d'identifiants et deux jeux de capacités complètement séparés, automatiquement.

typescript
// POST /connect  { connector, values }
async function connectForUser(claims: AuthedPrincipal, connector: string, values: Record<string, string>) {
  const handle = vinkius.user(externalIdFor(claims)).connector(connector);
  await handle.connect();
  await handle.credentials.set(values);
  return handle.status();
}

5. Politiques par tenant sur une clé partagée

Vous devrez généralement proposer des connecteurs différents par forfait, ou par tenant. Comme l'acteur est cloisonné par espace de noms, la configuration au niveau tenant se combine à la connexion au niveau utilisateur sans aucune notion spéciale du SDK : conservez la liste d'autorisation par tenant dans votre base de données et transmettez-la comme include.

typescript
// server/policy.ts
export async function allowedConnectors(tenantId: string): Promise<string[]> {
  // ex. : les tenants enterprise obtiennent 'salesforce' et 'snowflake'
  return await billing.planAllows(tenantId);
}

async function capabilitiesFor(claims: AuthedPrincipal) {
  const allowed = await allowedConnectors(claims.tenantId);
  return vinkius
    .user(externalIdFor(claims))
    .capabilities({ include: allowed }); // réduit le fan-out aux seuls connecteurs autorisés
}

include émonde le fan-out avant tout appel au runtime : un connecteur interdit par le forfait du tenant n'est jamais interrogé, si bien que l'utilisateur ne le voit jamais, même si cette connexion existe. Politique et connectivité se combinent proprement.

6. Utiliser le bon adaptateur pour des environnements de modèles hétérogènes

Des tenants différents (ou des fonctionnalités différentes) peuvent s'exécuter sur des modèles différents. Comme CapabilitySet est indépendant du framework, un seul chemin de code les sert tous : convertissez à la dernière ligne.

typescript
import { toOpenAITools } from '@vinkius/connect/openai';
import { toAnthropicTools } from '@vinkius/connect/anthropic';
import { toGeminiTools } from '@vinkius/connect/gemini';

const capabilities = await capabilitiesFor(claims);

const toolSpec =
  model === 'openai' ? toOpenAITools(capabilities)
  : model === 'anthropic' ? toAnthropicTools(capabilities)
  : model === 'gemini' ? toGeminiTools(capabilities)
  : capabilities; // conservez l'ensemble brut pour l'exécution

Conservez les capabilities d'origine pour l'exécution, seules les définitions converties sont remises au modèle. Les helpers de dispatch de l'adaptateur réacheminent l'outil choisi vers la connexion du bon utilisateur.

7. Les tenants enterprise qui exigent leur propre application

Certains clients enterprise exigent une clé de tenant dédiée au lieu de partager la vôtre. Ce n'est qu'une autre instance de Vinkius, sélectionnée par requête, votre logique d'externalIdFor ne change pas.

typescript
import { Vinkius } from '@vinkius/connect';

const shared = new Vinkius({
  appId: process.env.VINKIUS_APP_ID!,
  apiKey: process.env.VINKIUS_APP_KEY!,
});

const dedicated = new Map<string, Vinkius>(); // tenantId -> sa propre app

function clientFor(tenantId: string): Vinkius {
  return dedicated.get(tenantId) ?? shared;
}

8. Traiter explicitement le cas du « mauvais tenant »

Défense en profondeur : si une requête référence un id que vous ne pouvez pas autoriser, traitez une NotFoundError comme un échec de périmètre, et non comme un 404 générique.

typescript
import { NotFoundError, AuthError } from '@vinkius/connect';

try {
  await capabilitiesFor(claims);
} catch (error) {
  if (error instanceof AuthError) return respond(401);
  if (error instanceof NotFoundError) return respond(403, 'out of scope'); // entre tenants
  throw error;
}

Deux garanties auxquelles tout tenant se fie

Une clé exposée ne franchit aucune frontière

Votre plateforme détient un seul secret vk_app_sk_*. Son rayon d'impact est délimité par conception : une exposition compromet une application, et toute tentative d'accéder à une ressource hors de cette application renvoie 404, jamais les données d'un autre tenant, jamais un 500 confirmant qu'une ressource existe. Un external_id mal routé échoue donc de façon sûre, ce qu'une revue de sécurité enterprise attend précisément.

Un nom avec espace de noms, beaucoup de règles de modèle

L'espace de noms par défaut connector__name n'est pas valide pour tout runtime vers lequel vous pourriez router un tenant. toGeminiTools rejette les tirets d'emblée (un slug de connecteur google-calendar enfreint la règle ^[a-zA-Z_][a-zA-Z0-9_]*$ de Gemini) et toOpenAITools plafonne les noms à 64 caractères, l'un et l'autre levant une ConfigError à la conversion, et non un 400 du fournisseur à l'inférence. Normalisez une fois pour qu'un tenant exécuté sur Gemini fonctionne aussi proprement qu'un tenant sur OpenAI :

typescript
new Vinkius({
  appId,
  apiKey,
  // uniquement des tirets bas, longueur plafonnée : valide pour OpenAI, Anthropic et Gemini
  namespaceCapability: (connector, name) =>
    `${connector}_${name}`.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 64),
});

Liste de contrôle de production

  • [ ] Composez toujours l'external_id à partir de claims de tenant et d'utilisateur authentifiés, jamais à partir de données brutes.
  • [ ] Gardez une seule clé d'Application pour la plateforme ; n'ajoutez des instances Vinkius dédiées que pour les tenants qui les exigent.
  • [ ] Appliquez les forfaits par tenant avec capabilities({ include }), adossés à votre table de facturation.
  • [ ] Comptez sur le 404 comme hors-périmètre : ne tentez jamais de synthétiser les ressources d'un autre tenant.
  • [ ] Convertissez avec les adaptateurs à l'appel au modèle ; exécutez sur le CapabilitySet conservé.
  • [ ] Utilisez un idempotencyKey par opération pour que les tentatives répétées d'un tenant ne se croisent ni ne se dupliquent jamais.

Vous exploitez désormais une unique plateforme d'IA qui sert chaque client et chaque utilisateur en son sein, chacun sur des connexions et des identifiants isolés, avec votre propre clé d'application et une couche de politique par tenant, et les identités de vos clients ne quittent jamais votre contrôle. Les concurrents doivent acheter cette capacité ou la construire pendant des années. Vous l'avez reçue le jour où vous avez créé votre Application, et cette avance se cumule avec chaque tenant que vous signez.

What you just got

Not a pitch: the properties this build inherits automatically.

Isolation by construction

Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.

Write-only credentials

Your server stores secrets and can read back which fields are configured, never the values. Not your code, the model, or a dashboard can exfiltrate them.

Metered, revocable spend

Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.

Any model runtime

One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.

Production safety built in

idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.

Enterprise-grade tenancy

One app key, every customer isolated by address; a cross-tenant attempt is a 404. A customer can even get their own Vinkius instance, same code.

Give it to your AI agent

An Agent Skill (SKILL.md) for this build. Preview the first lines below, then copy or download it into your repo under .claude/skills/: Claude Code, Cursor or any Agent-Skills-compatible agent follows it to implement this pattern correctly.

Download SKILL.md6 · Available in your language
What a multi-tenant SaaS inherits, plus the SKILL.md, in your language, for your coding agent.

Prochaines étapes