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

Un SaaS de IA multi-tenant

Pregunta a la IA sobre Vinkius

Entregue IA con conectores a cada cliente de su plataforma B2B: delimite cada external_id por tenant y usuario, mantenga cada organización aislada con una única clave de aplicación y añada control por tenant sin exponer nunca una credencial a través de la frontera. Sus clientes reciben una plataforma de conectividad; usted nunca les entrega la infraestructura.

Lanza el SaaS de IA que tu categoría estaba esperando: cada cliente (tenant) recibe un equipo de usuarios, cada usuario recibe su IA accediendo a su GitHub, Jira y Slack, cada tenant aislado en su única Application key, y cada usuario de cada tenant respaldado por miles de conexiones de IA desde el primer día. Ninguna integración construida por usted, ningún token almacenado por usted, ninguna identidad saliendo de su base de datos.

Toda plataforma de integración del mercado le dirá que ese problema termina en un contrato por tenant, una factura por tenant, o meses de sus ingenieros construyendo el aislamiento a mano. La respuesta del AI Connect SDK es la que ningún competidor puede copiar sin rearquitecturar su producto: entregue funciones de IA, no proyectos de integración. Esta guía le muestra cómo hacer que todo un marketplace de tenants comparta una única clave de aplicación sin cruzar nunca una frontera.

Aquí el "usuario" es una persona dentro de su cliente, así que el external_id debe llevar tanto el tenant como al humano. Esa única decisión es el modelo de aislamiento. Cuando funciona, su producto hace lo que normalmente le cuesta años a una empresa de plataforma y a un equipo de seguridad prometer: cada organización es una isla, cada usuario es ciudadano de exactamente una isla, y usted administra todo el archipiélago desde una sola clave. Vinkius nunca conoce a sus usuarios reales. Sus correos, sus nombres y sus perfiles nunca salen de su base de datos; la plataforma solo ve el id opaco que su backend le entrega.

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.

El contrato de aislamiento

  • Una Application = su producto. Todos los tenants comparten normalmente su único appId.
  • El external_id codifica la dirección. cus_<tenant>_u_<user> es la frontera; las capacidades solo se resuelven dentro de ella.
  • El cruce entre tenants es un 404, no un 500. Un id expuesto o mal enrutado no puede leer la conexión de otro tenant, Vinkius lo rechaza por estar fuera de alcance.
  • Sus usuarios permanecen bajo su control. Ningún correo ni perfil llega a Vinkius; usted le entrega solo un id opaco. Su relación con el cliente permanece bajo su control.

El aislamiento se deriva de un external_id bien formado, así que trátelo como una entrada crítica para la seguridad. Constrúyalo siempre a partir de claims de tenant y usuario autenticados, nunca de datos crudos de la solicitud, y nunca permita que un tenant suministre el id de otro. Para el modelo profundo de garantías, consulte Autenticación y alcance y Seguridad.

1. Direccionar a un usuario dentro de un tenant

Componga un id determinista y seguro para URL a partir de los dos ids en los que su autenticación ya confía.

typescript
interface AuthedPrincipal { tenantId: string; userId: string } // de su JWT/sesión

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

2. Un único cliente compartido

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. Resolver el actor desde una sesión verificada

Derive el principal de su token y páselo al SDK. Todo lo posterior queda delimitado porque externalIdFor lo está.

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

export async function actorFor(req: Request) {
  const claims = await verifySession(req); // su autenticación/autorización
  if (!claims) throw new Error('unauthenticated');
  return vinkius.user(externalIdFor(claims));
}

4. Permitir que cada usuario conecte sus propias herramientas

Dos personas en dos empresas conectan la misma integración de GitHub y obtienen dos conexiones, credenciales y capacidades completamente separadas, automáticamente.

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. Políticas por tenant sobre una clave compartida

Normalmente necesitará ofrecer conectores diferentes por plan, o por tenant. Como el actor está en un espacio de nombres, la configuración a nivel de tenant se compone con la conexión a nivel de usuario sin ningún concepto especial del SDK: guarde la lista permitida por tenant en su base de datos y pásela como include.

typescript
// server/policy.ts
export async function allowedConnectors(tenantId: string): Promise<string[]> {
  // p. ej., los tenants enterprise obtienen 'salesforce' y '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 }); // recorta el fan-out solo a los conectores permitidos
}

include recorta el fan-out antes de cualquier llamada al runtime: un conector que el plan del tenant prohíbe nunca se consulta, así que el usuario simplemente nunca lo ve, incluso si esa conexión existe. La política y la conectividad se componen de forma limpia.

6. Usar el adapter adecuado para entornos de modelo heterogéneos

Distintos tenants (o distintas funciones) pueden ejecutarse en modelos diferentes. Como CapabilitySet es independiente del framework, una única ruta de código los sirve a todos: convierta en la última línea.

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; // conserve el conjunto crudo para ejecutar sobre él

Conserve los capabilities originales para la ejecución, solo las definiciones convertidas se entregan al modelo. Los helpers de despacho del adapter reenvían la herramienta elegida a la conexión del usuario correcto.

7. Tenants enterprise que exigen su propia aplicación

Algunos clientes enterprise exigen una clave de tenant dedicada en lugar de compartir la suya. Eso es simplemente otra instancia de Vinkius, seleccionada por solicitud, su lógica de externalIdFor no cambia.

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 -> su propia app

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

8. Tratar explícitamente el caso del «tenant equivocado»

Defensa en profundidad: si una solicitud hace referencia a un id que usted no puede autorizar, trate un NotFoundError como un fallo de alcance, no como un 404 genérico.

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;
}

Dos garantías en las que todo tenant confía

Una clave expuesta no cruza la frontera

Su plataforma guarda un único secreto vk_app_sk_*. Su radio de daño está acotado por diseño: una exposición compromete una aplicación, y cualquier intento de acceder a un recurso fuera de esa aplicación devuelve 404, nunca los datos de otro tenant, nunca un 500 que confirme que un recurso existe. Por tanto, un external_id mal enrutado falla de forma segura, que es exactamente lo que espera una revisión de seguridad enterprise.

Un nombre con espacio de nombres, muchas reglas de modelo

El espacio de nombres por defecto connector__name no es válido para todos los entornos de ejecución a los que pueda enrutar un tenant. toGeminiTools rechaza los guiones de inmediato (un slug de conector google-calendar viola la regla ^[a-zA-Z_][a-zA-Z0-9_]*$ de Gemini) y toOpenAITools limita los nombres a 64 caracteres, ambos lanzan ConfigError al convertir, no un 400 del proveedor al inferir. Normalice una vez para que un tenant que se ejecuta en Gemini funcione con la misma limpieza que uno en OpenAI:

typescript
new Vinkius({
  appId,
  apiKey,
  // solo guiones bajos, con límite de longitud: válido para OpenAI, Anthropic y Gemini por igual
  namespaceCapability: (connector, name) =>
    `${connector}_${name}`.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 64),
});

Lista de verificación de producción

  • [ ] Componga siempre el external_id a partir de claims autenticados de tenant + usuario, nunca de entrada cruda.
  • [ ] Mantenga una Application key para la plataforma; añada instancias Vinkius dedicadas solo para tenants que las exijan.
  • [ ] Aplique planes por tenant con capabilities({ include }), respaldados por su tabla de facturación.
  • [ ] Confíe en el 404-como-fuera-de-alcance: nunca intente sintetizar recursos de otro tenant.
  • [ ] Convierta con adapters en la llamada al modelo; ejecute sobre el CapabilitySet conservado.
  • [ ] Use idempotencyKey por operación para que los reintentos de un tenant nunca se crucen ni se dupliquen.

Ahora opera una única plataforma de IA que atiende a cada cliente y a cada usuario dentro de él, cada uno sobre conexiones y credenciales aisladas, con su propia clave de aplicación y una capa de políticas por tenant, y las identidades de sus clientes nunca salen de su control. Los rivales tienen que comprar esta capacidad o construirla durante años. Usted la recibió el día en que creó su Application, y esa ventaja se compone con cada tenant que firma.

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.

Próximos pasos