Equipe
Modelo de equipe do Hub: organizações como tenant, papéis OWNER/ADMIN/MEMBER/CLIENT, fluxo de convites, bypass interno via MCP_INTERNAL_SECRET e troca de organização.
Equipe
O Hub é multi-tenant por organização. Todo recurso operacional (agentes, credenciais, leads, conversas, integrações) é escopado a uma Organization, e cada usuário acessa esses recursos por meio de uma Membership que carrega o papel (Role) dentro daquela organização.
Esta página descreve o modelo de tenancy, os papéis disponíveis, o fluxo de convites, o bypass de admin interno usado por chamadas server-to-server e a mecânica de troca de organização exposta pela API.
Modelo de organização
O tenant primário é o modelo Organization. Cada organização tem id, slug único e name, e funciona como raiz de isolamento para todos os recursos do Hub.
Um usuário pode pertencer a múltiplas organizações por meio de Membership. A constraint @@unique([userId, organizationId]) garante que cada usuário tenha no máximo um vínculo (e um papel) por organização.
model Organization {
id String @id @default(cuid())
name String
slug String @unique
avatarUrl String?
memberships Membership[]
invitations Invitation[]
credentials Credential[]
// ... demais relacionamentos escopados por org
}
model Membership {
id String @id @default(cuid())
userId String
organizationId String
role Role @default(MEMBER)
clientPermissions ClientPermission[]
permissions Permission[] @relation("MembershipPermissions")
@@unique([userId, organizationId])
}Toda chamada à API que toca recursos de uma organização passa por requireOrgMembership(orgId) antes de executar a operação. Se o usuário autenticado não tem Membership ativo na organização do recurso, a chamada falha com Not a member of this organization.
Papéis e permissões
O enum Role define quatro papéis fixos:
| Papel | Descrição |
|---|---|
OWNER | Dono da organização. Único papel autorizado por requireOrgOwner. Pode transferir titularidade e excluir org. |
ADMIN | Administrador operacional. Passa por requireOrgAdmin junto com OWNER. |
MEMBER | Operador padrão da organização. Acessa módulos sem o gating granular aplicado a CLIENT. |
CLIENT | Acesso restrito por módulo via ClientPermission. Usado pelo portal do cliente. |
A hierarquia é aplicada por helpers em apps/web/src/lib/auth/org-auth.ts:
// Qualquer membro
await requireOrgMembership(orgId);
// OWNER ou ADMIN
await requireOrgAdmin(orgId);
// Apenas OWNER
await requireOrgOwner(orgId);Permissões granulares para CLIENT
Usuários com papel CLIENT têm acesso adicional controlado por ClientPermission, com um registro por módulo e nível (none | view | edit).
Os módulos suportados estão em apps/web/src/lib/auth/client-permissions.ts:
export const CLIENT_MODULES = [
"dashboard",
"inbox",
"funnel",
"contacts",
"agents",
"rag",
"metrics",
"channels",
] as const;requireClientPermission(orgId, module, minLevel) aplica o gating:
- Se o papel da membership for
OWNER,ADMINouMEMBER, o gating é ignorado (operadores têm acesso pleno). - Se for
CLIENT, o helper consultaClientPermissiondo módulo e compara o nível armazenado contraminLevel(view<edit). Sem registro ou nível insuficiente, lançaInsufficient permissions.
Não conceda papel MEMBER para usuários externos. MEMBER ignora ClientPermission e tem visão operacional completa. Para acesso restrito ao portal, sempre use CLIENT com ClientPermission ajustado por módulo.
Convidar membros
Convites são modelados como registros Invitation com token único, papel pré-atribuído, expiração e status. O ciclo de vida está exposto sob /api/organizations/[orgId]/invitations (criação/listagem), /api/organizations/[orgId]/invitations/[invitationId] (revogação) e /api/organizations/invitations/accept (aceite).
model Invitation {
id String @id @default(cuid())
email String
organizationId String
role Role @default(MEMBER)
status InvitationStatus @default(PENDING)
invitedById String
token String @unique @default(cuid())
expiresAt DateTime
permissionsConfig Json?
}
enum InvitationStatus {
PENDING
ACCEPTED
EXPIRED
REVOKED
}Criar o convite. A chamada precisa de papel mínimo ADMIN na organização. O payload define email, role e, para convites CLIENT, permissionsConfig com os níveis por módulo.
curl -X POST https://hub.simplafy.com.br/api/v1/organizations/$ORG_ID/invitations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"role": "CLIENT",
"permissionsConfig": {
"dashboard": "view",
"inbox": "edit"
}
}'Enviar o link. O Hub gera um token único. O link de aceite tem o formato:
https://hub.simplafy.com.br/invitations/accept?token=<token>O destinatário recebe o convite por e-mail (quando o envio está habilitado) ou via repasse manual do link.
Aceitar o convite. Se o e-mail já tem conta no Hub, o aceite acontece via POST /api/organizations/invitations/accept com o token. Se ainda não tem conta, o fluxo accept-with-signup cria o usuário e a Membership na mesma transação.
O aceite cria a Membership com o role do convite. Para convites CLIENT, os registros de ClientPermission são materializados a partir de permissionsConfig (ou dos defaults via createDefaultClientPermissions quando o config é omitido).
Revogar. Convites pendentes podem ser revogados por ADMIN ou OWNER via DELETE /api/organizations/$ORG_ID/invitations/$INVITATION_ID. O status passa a REVOKED e o token deixa de ser aceitável. Convites expirados (expiresAt < now) são marcados como EXPIRED automaticamente no momento do aceite.
Internal admin bypass
Chamadas server-to-server (admin proxies, MCP server, jobs internos) precisam atravessar as mesmas verificações de organização, mas não têm sessão NextAuth. O Hub suporta um bypass dedicado controlado pela variável MCP_INTERNAL_SECRET.
A lógica está em apps/web/src/lib/auth/internal-auth.ts e é invocada pelos helpers de org-auth.ts:
async function isInternalAdmin(): Promise<boolean> {
if (!process.env.MCP_INTERNAL_SECRET) return false;
try {
await validateInternalAuth();
return true;
} catch {
return false;
}
}validateInternalAuth extrai o Bearer token do header Authorization e compara contra MCP_INTERNAL_SECRET usando comparação timing-safe (timingSafeCompare).
Quando o bypass é aceito:
requireAuthretorna o sentinel"__internal_admin__"comouserId.requireOrgMembershipretorna{ userId: "__internal_admin__", membership: { role: "OWNER" } }sem consultar o banco.- Por consequência,
requireOrgAdminerequireOrgOwnertambém passam.
Exemplo de chamada interna:
curl https://hub.simplafy.com.br/api/v1/organizations/$ORG_ID/agents \
-H "Authorization: Bearer $MCP_INTERNAL_SECRET"O header legado x-internal-admin: true foi removido. Qualquer caller interno precisa enviar Authorization: Bearer ${MCP_INTERNAL_SECRET}. O segredo é distribuído via Infisical para as workloads que precisam dele e nunca deve ser exposto a clientes ou ao browser.
Se MCP_INTERNAL_SECRET não está configurado no pod, isInternalAdmin retorna false antes de qualquer tentativa de validação — o bypass simplesmente não existe naquele ambiente, e as chamadas precisam usar sessão real ou API key.
Trocar de organização
Como Membership é n:n entre User e Organization, o usuário pode estar vinculado a várias orgs. O Hub mantém uma organização ativa por sessão, exposta no contexto da aplicação web e no token JWT consumido pela API.
O OrgProvider (parte da chain Sidebar → Org → MCP → Agents → Rag em apps/web) carrega as memberships do usuário, persiste a organização selecionada e atualiza o contexto downstream (MCP, agentes, RAG).
Pontos importantes ao integrar:
- A escolha de organização é feita pelo usuário; não há herança implícita de "última organização usada" entre dispositivos.
- O reset do estado downstream (agentes, deployments, channels) e o fetch dos novos dados ocorrem em um único
useEffectcomprevOrgIdref. Dividir em dois efeitos separados causa race entre reset e fetch. - Toda chamada server-side revalida a organização via
requireOrgMembership(orgId)— não basta confiar noorgIdarmazenado em cache do cliente. - Para chamadas server-to-server que operam sobre várias organizações em sequência, use o bypass
MCP_INTERNAL_SECRETem vez de simular troca de sessão.
Integrações
Visão geral das integrações nativas do Hub: WhatsApp via Evolution API, email via SMTP, workflows n8n e ferramentas MCP, com instruções para adicionar novas integrações.
Observabilidade
Como observar a Hub API em produção — BI no admin, traces em Langfuse, erros em GlitchTip e alertas operacionais.