AI Connect/Como criar/Um SaaS de IA multi-tenant

Um SaaS de IA multi-tenant

Pergunte à IA sobre a Vinkius

Entregue IA com conectores a cada cliente da sua plataforma B2B: delimite cada external_id por tenant e usuário, mantenha cada organização isolada em uma única chave de aplicação e adicione controle por tenant sem jamais expor uma credencial através da fronteira. Seus clientes recebem uma plataforma de conectividade; você nunca entrega a infraestrutura a eles.

Lance o SaaS de IA que a sua categoria estava esperando: cada cliente (tenant) ganha um time de usuários, cada usuário ganha a sua IA acessando o seu GitHub, Jira e Slack, cada tenant isolado na sua única Application Key, e cada usuário de cada tenant apoiado em milhares de conexões de IA desde o primeiro dia. Nenhuma integração construída por você, nenhum token armazenado por você, nenhuma identidade saindo do seu banco.

Toda plataforma de integração do mercado vai dizer que esse problema termina num contrato por tenant, numa fatura por tenant, ou em meses dos seus engenheiros construindo o isolamento à mão. A resposta do AI Connect SDK é a que nenhum concorrente consegue copiar sem rearquitetar o produto: entregue recursos de IA, não projetos de integração. Este guia mostra como fazer um marketplace inteiro de tenants compartilhar uma única chave de aplicação sem jamais cruzar uma fronteira.

Aqui o "usuário" é uma pessoa dentro do seu cliente, então o external_id precisa carregar tanto o tenant quanto o humano. Essa única decisão é o modelo de isolamento. Quando funciona, o seu produto faz o que normalmente leva anos de uma empresa de plataforma e um time de segurança para prometer: cada organização é uma ilha, cada usuário é cidadão de exatamente uma ilha, e você administra o arquipélago inteiro a partir de uma chave. A Vinkius nunca encontra os seus usuários reais. E-mails, nomes e perfis deles nunca saem do seu banco; a plataforma só enxerga o id opaco que o seu backend 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.

O contrato de isolamento

  • Uma Application = seu produto. Todos os tenants normalmente compartilham seu único appId.
  • O external_id codifica o endereço. cus_<tenant>_u_<usuario> é a fronteira; as capacidades resolvem apenas dentro dela.
  • Cruzamento de tenant é 404, não 500. Um id exposto ou mal roteado não consegue ler a conexão de outro tenant, o Vinkius o rejeita por estar fora do escopo.
  • Seus usuários permanecem sob seu controle. Nenhum e-mail ou perfil chega ao Vinkius; você entrega apenas um id opaco. Seu relacionamento com o cliente permanece sob seu controle.

O isolamento deriva de um external_id bem formado, então trate-o como entrada de segurança. Sempre o construa a partir de claims autenticados de tenant e usuário, jamais de dados crus da requisição, e jamais deixe um tenant fornecer o id de outro. Para o modelo profundo de garantias, veja Autenticação e escopo e Segurança.

1. Enderece um usuário dentro de um tenant

Componha um id determinístico e seguro para URL a partir dos dois ids em que sua autenticação já confia.

typescript
interface AuthedPrincipal { tenantId: string; userId: string } // do seu JWT/sessão

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

2. Um cliente compartilhado

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. Resolva o ator a partir de uma sessão verificada

Derive o principal do seu token e passe-o ao SDK. Tudo a jusante fica com escopo porque externalIdFor fica.

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

export async function actorFor(req: Request) {
  const claims = await verifySession(req); // sua autenticação/autorização
  if (!claims) throw new Error('não autenticado');
  return vinkius.user(externalIdFor(claims));
}

4. Deixe cada usuário conectar as próprias ferramentas

Duas pessoas em duas empresas conectam a mesma integração do GitHub e obtêm duas conexões, credenciais e capacidades completamente separadas, automaticamente.

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 uma chave compartilhada

Normalmente, você precisará oferecer conectores diferentes por plano, ou por tenant. Como o ator tem namespace, a configuração em nível de tenant compõe com a conexão em nível de usuário sem nenhum conceito especial do SDK: guarde a allow-list por tenant no seu banco de dados e passe-a como include.

typescript
// server/policy.ts
export async function allowedConnectors(tenantId: string): Promise<string[]> {
  // ex.: tenants enterprise ganham 'salesforce' e '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 }); // reduz o fan-out só aos conectores permitidos
}

O include poda o fan-out antes de qualquer chamada ao runtime: um conector que o plano do tenant proíbe jamais é consultado, então o usuário simplesmente nunca o vê, mesmo que a conexão exista. Política e conectividade compõem de forma limpa.

6. Use o adapter certo para runtimes de modelo heterogêneos

Tenants diferentes (ou features diferentes) podem rodar em modelos diferentes. Como CapabilitySet é agnóstico de framework, um único caminho de código atende a todos: converta na última linha.

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; // mantenha o conjunto bruto para executar

Mantenha o capabilities original para a execução, apenas as definições convertidas são entregues ao modelo. Os helpers de despacho do adapter roteiam a tool escolhida de volta para a conexão do usuário correto.

7. Tenants enterprise que exigem a própria aplicação

Alguns clientes enterprise exigem uma chave de tenant dedicada em vez de compartilhar a sua. Isso é simplesmente outra instância do Vinkius, selecionada por requisição, sua lógica de externalIdFor não muda.

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 -> app própria

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

8. Trate explicitamente o caso "tenant errado"

Em profundidade: se uma requisição referenciar um id que você não pode autorizar, trate um NotFoundError como falha de escopo, não como um 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, 'fora de escopo'); // cross-tenant
  throw error;
}

Duas garantias em que todo tenant confia

Uma chave exposta não cruza a fronteira

Sua plataforma guarda um único segredo vk_app_sk_*. O raio de impacto é delimitado por design: uma exposição compromete uma aplicação, e qualquer tentativa de acessar um recurso fora daquela aplicação retorna 404, jamais os dados de outro tenant, jamais um 500 que confirme que um recurso existe. Um external_id mal roteado, portanto, falha de forma segura, exatamente o que uma revisão de segurança enterprise espera.

Um nome com namespace, muitas regras de modelo

O namespace padrão connector__name não é válido para todo runtime em que você pode rotear um tenant. O toGeminiTools rejeita hífens de imediato (um slug google-calendar viola a regra ^[a-zA-Z_][a-zA-Z0-9_]*$ do Gemini) e o toOpenAITools limita nomes a 64 caracteres, ambos lançando ConfigError na conversão, não um 400 do provedor na inferência. Normalize uma vez, para que um tenant rodando no Gemini funcione de forma tão limpa quanto um no OpenAI:

typescript
new Vinkius({
  appId,
  apiKey,
  // só sublinhado, com limite: válido para OpenAI, Anthropic e Gemini
  namespaceCapability: (connector, name) =>
    `${connector}_${name}`.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 64),
});

Checklist de produção

  • [ ] Sempre componha o external_id de claims autenticados de tenant + usuário, nunca de entrada crua.
  • [ ] Mantenha uma Application key para a plataforma; adicione instâncias Vinkius dedicadas só para tenants que as exijam.
  • [ ] Aplique planos por tenant com capabilities({ include }), lastreado na sua tabela de cobrança.
  • [ ] Confie no 404-como-fora-de-escopo: jamais tente sintetizar recursos de outro tenant.
  • [ ] Converta com adapters na chamada ao modelo; execute sobre o CapabilitySet retido.
  • [ ] Use idempotencyKey por operação para que tentativas de um tenant nunca dobrem ou cruzem.

Você agora opera uma única plataforma de IA que atende cada cliente e cada usuário dentro dele, cada um sobre conexões e credenciais isoladas, com a sua própria chave de aplicação e uma camada de política por tenant, e as identidades dos seus clientes jamais saem do seu controle. Concorrentes precisam comprar essa capacidade ou construí-la por anos. Você a recebeu no dia em que criou a sua Application, e essa vantagem se acumula a cada tenant que assina.

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 passos