O cenário
Você está construindo um ERP para clínicas médicas. Cada clínica é uma empresa diferente — os dados de pacientes da Clínica A nunca podem se misturar com os da Clínica B. Você precisa:
- Isolamento físico de dados (LGPD, HIPAA-like)
- Onboarding automático quando uma nova clínica se cadastra
- API keys separadas por cliente para as queries do frontend
- Capacidade de apagar todos os dados de um cliente com uma operação
Este é o caso perfeito para o SuperDB Groups.
Tempo para implementar: 2-4 horas para um dev familiarizado com Next.js/Node.js. O provisioning de tenant leva ~1-2 segundos.
Passo a passo
1Criar o projeto template
No dashboard (app.superdb.com.br), crie um novo projeto chamado erp_clinica_template. Este projeto é o molde — você nunca o usará diretamente com clientes reais.
Nomeie com sufixo _template para diferenciar visualmente dos projetos de produção.
app.superdb.com.br/projects
└── + Novo projeto
Nome: erp_clinica_template
Slug: erp_clinica_template (auto)
2Modelar o schema no SQL Editor
Abra o SQL Editor do projeto template e crie as tabelas do domínio. Inclua RLS — ela será clonada para cada tenant.
Este schema é copiado integralmente para cada novo tenant provisionado.
-- Tabela de pacientes
CREATE TABLE pacientes (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
nome text NOT NULL,
cpf text UNIQUE,
data_nasc date,
email text,
telefone text,
created_at timestamptz DEFAULT now(),
created_by uuid REFERENCES auth_users(id)
);
-- Tabela de médicos
CREATE TABLE medicos (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
nome text NOT NULL,
crm text NOT NULL UNIQUE,
especialidade text,
user_id uuid REFERENCES auth_users(id)
);
-- Tabela de atendimentos
CREATE TABLE atendimentos (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
paciente_id uuid REFERENCES pacientes(id) ON DELETE CASCADE,
medico_id uuid REFERENCES medicos(id),
data_hora timestamptz NOT NULL,
observacoes text,
created_at timestamptz DEFAULT now()
);
-- RLS: médicos só veem seus próprios atendimentos
ALTER TABLE atendimentos ENABLE ROW LEVEL SECURITY;
CREATE POLICY atendimentos_medico ON atendimentos
USING (
medico_id IN (
SELECT id FROM medicos WHERE user_id = auth.uid()
)
);
-- RLS: pacientes visíveis para qualquer autenticado da clínica
ALTER TABLE pacientes ENABLE ROW LEVEL SECURITY;
CREATE POLICY pacientes_authenticated ON pacientes
FOR ALL USING (auth.role() = 'authenticated');
3Criar o Group no Admin UI
Acesse admin.superdb.com.br → Groups → + Novo Group.
Selecione o projeto erp_clinica_template como template. Após criar, vá em API Keys e gere a Group API Key.
Copie a API Key agora. Ela só aparece uma vez. Cole no .env do seu backend imediatamente.
Platform → Groups → + Novo Group
Nome: ERP Clínicas
Template: erp_clinica_template
[Criar]
→ Aba "API Keys" → [Gerar API Key]
⚠️ Copie agora: grp_key_a1b2c3d4...
4Configurar variáveis de ambiente
No backend do seu SaaS (Node.js/Next.js), configure as variáveis de ambiente com as credenciais do Group.
SUPERDB_BASE_URL=https://auth.superdb.com.br
SUPERDB_GROUP_ID=grp_a1b2c3d4e5f6
SUPERDB_GROUP_API_KEY=grp_key_a1b2c3d4e5f6g7h8i9j0...
# Banco do SEU app (onde você guarda dados das empresas clientes)
DATABASE_URL=postgresql://user:pass@host/meusaas
5Função de provisioning
Crie o módulo que seu backend vai chamar quando uma nova clínica se cadastrar. Esta função chama a API do SuperDB e retorna as keys do tenant.
interface TenantProvisionResult {
tenant: {
id: string
slug: string
schema_name: string
created_at: string
}
anon_key: string
service_role_key: string
}
export async function provisionTenant(params: {
slug: string
externalId: string
metadata?: Record<string, unknown>
}): Promise<TenantProvisionResult> {
const url = `${process.env.SUPERDB_BASE_URL}/groups/v1/${process.env.SUPERDB_GROUP_ID}/tenants`
const resp = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUPERDB_GROUP_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
slug: params.slug,
external_id: params.externalId,
metadata: params.metadata,
}),
})
if (!resp.ok) {
const body = await resp.text()
throw new Error(`Provisioning falhou (${resp.status}): ${body}`)
}
return resp.json() as Promise<TenantProvisionResult>
}
export async function deleteTenant(tenantId: string): Promise<void> {
const url = `${process.env.SUPERDB_BASE_URL}/groups/v1/${process.env.SUPERDB_GROUP_ID}/tenants/${tenantId}`
const resp = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${process.env.SUPERDB_GROUP_API_KEY}`,
},
})
if (!resp.ok && resp.status !== 204) {
throw new Error(`Deleção do tenant falhou: ${resp.status}`)
}
}
6Rota de onboarding
Integre o provisioning na rota de cadastro do seu SaaS. Quando uma nova empresa se cadastra, chame provisionTenant e salve as keys no banco do seu app.
As keys ficam no banco do SEU app — você as fornece ao frontend da clínica para que ela faça queries autenticadas.
import { NextResponse } from 'next/server'
import { provisionTenant } from '@/lib/superdb-groups'
import { db } from '@/lib/db' // seu banco de dados (Prisma, Drizzle, etc.)
export async function POST(request: Request) {
const { companyName, cnpj, adminEmail, plan } = await request.json()
// 1. Validar dados
if (!companyName || !cnpj || !adminEmail) {
return NextResponse.json({ error: 'Dados incompletos' }, { status: 400 })
}
// 2. Criar empresa no banco do SEU app primeiro
const company = await db.companies.create({
data: {
name: companyName,
cnpj,
admin_email: adminEmail,
plan,
status: 'provisioning',
}
})
try {
// 3. Provisionar tenant no SuperDB
const slug = companyName
.toLowerCase()
.normalize('NFD')
.replace(/[̀-ͯ]/g, '') // remover acentos
.replace(/[^a-z0-9]/g, '_')
.replace(/_+/g, '_')
.slice(0, 50)
const result = await provisionTenant({
slug,
externalId: company.id,
metadata: { cnpj, plan, admin_email: adminEmail },
})
// 4. Salvar keys no banco do SEU app
// ⚠️ service_role_key deve ser encriptada em repouso
await db.companies.update({
where: { id: company.id },
data: {
superdb_tenant_id: result.tenant.id,
superdb_tenant_slug: result.tenant.slug,
superdb_anon_key: result.anon_key,
superdb_service_key: encrypt(result.service_role_key), // criptografar!
status: 'active',
}
})
return NextResponse.json({
ok: true,
company_id: company.id,
superdb_url: process.env.SUPERDB_BASE_URL,
anon_key: result.anon_key, // pode ir pro frontend
})
} catch (error) {
// Rollback: marcar empresa como failed
await db.companies.update({
where: { id: company.id },
data: { status: 'provisioning_failed' }
})
throw error
}
}
7Frontend da clínica usa as keys do tenant
Quando um usuário da Clínica A faz login no seu app, você fornece as keys do tenant A para o SDK. As queries ficam isoladas no schema da Clínica A — o RLS e o isolamento de schema garantem isso em duas camadas.
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: Request) {
// Usuário já autenticado no SEU SaaS (sessão do seu app, não do SuperDB)
const session = await getMyAppSession(request)
if (!session) return NextResponse.json({ error: 'Não autorizado' }, { status: 401 })
const company = await db.companies.findUnique({
where: { id: session.companyId },
select: {
superdb_anon_key: true,
superdb_tenant_slug: true,
}
})
return NextResponse.json({
superdb_url: process.env.NEXT_PUBLIC_SUPERDB_URL,
anon_key: company.superdb_anon_key, // seguro — é a anon key
tenant_slug: company.superdb_tenant_slug,
})
}
// Buscar keys do tenant do servidor (após login no SEU app)
const { anon_key, tenant_slug } = await fetch('/api/session').then(r => r.json())
// Dados via HTTP direto — api.superdb.com.br (sem /rest/v1/)
// Header Accept-Profile seleciona o schema do tenant
// O PostgREST também resolve via sub+lookup automaticamente
const res = await fetch('https://api.superdb.com.br/pacientes?select=id,nome,cpf,data_nasc&order=nome', {
headers: {
'Authorization': `Bearer ${anon_key}`,
'apikey': anon_key,
'Accept-Profile': `proj_${tenant_slug}`, // schema do tenant
},
})
const pacientes = await res.json()
// Esta query só retorna pacientes DA CLÍNICA DO USUÁRIO LOGADO
// Isolamento em duas camadas: schema separado + RLS
8Testar o fluxo completo
Com o servidor rodando, faça uma chamada de teste para o endpoint de onboarding e verifique se o tenant foi criado no Admin UI.
curl -X POST http://localhost:3000/api/onboarding \
-H "Content-Type: application/json" \
-d '{
"companyName": "Clínica São Lucas",
"cnpj": "12.345.678/0001-99",
"adminEmail": "dr.pedro@saolucas.com.br",
"plan": "professional"
}'
# Resposta esperada (~1-2s):
{
"ok": true,
"company_id": "clj_abc123",
"superdb_url": "https://auth.superdb.com.br",
"anon_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
admin.superdb.com.br
→ Groups → ERP Clínicas
→ Tenants (1)
└── clinica_sao_lucas
schema: proj_clinica_sao_lucas
created: agora
Considerações de segurança
- Service Role Key: criptografe em repouso no banco do seu app (AES-256-GCM). Nunca logue, nunca exponha no frontend.
- Group API Key: rotacione periodicamente. Revogue imediatamente se suspeitar de vazamento.
- Anon Key: pode ir ao frontend — só acessa dados via RLS do tenant específico.
- LGPD — Art. 18: para apagar todos os dados de um cliente: chame
DELETE /groups/v1/:group_id/tenants/:id. O SuperDB executaDROP SCHEMA CASCADEatomicamente.
Próximos passos
- Referência completa da API Groups
- Visão geral e arquitetura do Groups
- Aprofundar nas políticas RLS
- Conceitos: comparação de estratégias multi-tenant
- Adicionar login Google para os usuários da clínica