Objetivo do Volume: Compreender o papel vital do banco de dados relacional como a única fonte da verdade de um negócio, entender a fragilidade fatal de tentar operar empresas em planilhas de Excel, dominar o ecossistema do PostgreSQL 16 com Supabase e implementar um esquema completo de banco de dados multitenant blindado com Row-Level Security (RLS).
1. Fundamentos para Não-Técnicos: A Morte do "Excel como Banco de Dados"
Muitos empreendedores e consultores iniciantes tentam construir seus primeiros sistemas de automação conectando agentes de IA diretamente a uma planilha do Google Sheets ou do Excel.
Nas primeiras 48 horas, com 5 mensagens por dia, tudo parece funcionar. Mas assim que a empresa cliente lança uma campanha e começa a receber dezenas de mensagens simultâneas no WhatsApp, a catástrofe acontece:
- Colisão de Escrita (Race Conditions): Duas pessoas ou dois agentes tentam atualizar a mesma linha da planilha no mesmo segundo. O Google Sheets trava, descarta uma das gravações e o pedido simplesmente desaparece no ar.
- Falta de Integridade Referencial: Alguém apaga o nome de um produto na aba A, mas ele continua sendo referenciado na aba B. O robô tenta cotar o produto, encontra um erro e para de responder a todos os clientes.
- Vazamento de Dados Catastrófico: Qualquer pessoa com o link da planilha tem acesso a todos os custos, margens, telefones de clientes e faturamentos da empresa.
A Analogia do Cofre Suíço com Gaveteiros: Uma planilha do Excel é como um caderno de papel espiral aberto em cima de uma mesa de escritório: qualquer um pode passar, rabiscar por cima, rasgar uma folha ou derramar café. O PostgreSQL 16 é um cofre-forte blindado em um banco suíço com milhares de gaveteiros eletrônicos: - Nenhuma gaveta pode ser aberta sem a credencial exata; - Se duas pessoas tentarem colocar dinheiro na mesma gaveta ao mesmo tempo, o sistema cria uma fila ordenada por microssegundos (transações ACID); - Se o computador do operador perder a energia no meio de uma gravação, o cofre desfaz a operação pela metade e mantém o estado anterior perfeitamente intacto, sem corromper um único centavo.
O Que é o Supabase?
O Supabase é a plataforma de dados moderna mais adotada no ecossistema global de IA. Ele não é um banco de dados proprietário ou exótico: ele é 100% puro PostgreSQL 16, envelopado com ferramentas de nível industrial:
- Autenticação Integrada: Controle de login seguro de usuários e equipes;
- APIs Instantâneas: Gera automaticamente endpoints REST e GraphQL ultravelozes para qualquer tabela criada;
- Websockets em Tempo Real: Notifica interfaces e atendentes humanos no exato milissegundo em que um novo orçamento é inserido no banco;
- Suporte Nativo a Vetores: A extensão
pgvectorjá vem pré-instalada de fábrica para busca semântica de IA.
2. A Arquitetura Multitenant: Atendendo 100 Empresas com 1 Único Banco
Se você atende múltiplos clientes corporativos ou deseja operar uma solução SaaS/White-Label, você se depara com um dilema de engenharia:
- Abordagem A (Bancos Isolados por Cliente): Criar uma instância de banco de dados separada para cada cliente que você fechar. Resultado: se você tiver 30 clientes, terá que gerenciar 30 bancos, pagar 30 faturas mínimas e rodar atualizações de esquema 30 vezes.
- Abordagem B (Multitenancy com
tenant_id): Ter um único cluster de alta performance do PostgreSQL e adicionar uma colunatenant_id(identificador único da empresa) em absolutamente todas as tabelas.
A Abordagem B é o padrão das maiores empresas de tecnologia do mundo (Stripe, Shopify, Salesforce). No entanto, ela traz um risco fatal para desenvolvedores novatos:
O Risco do Esquecimento Humano: Se o programador esquecer de adicionar WHERE tenant_id = 'empresa_a' em uma única consulta SQL no código da aplicação, o sistema poderá exibir os orçamentos sigilosos da Empresa A na tela da Empresa B, gerando processos judiciais graves e quebra de contratos sob a LGPD.
A Solução Definitiva: Row-Level Security (RLS)
Para eliminar 100% do risco de erro humano no código, o PostgreSQL oferece um recurso nativo de nível militar chamado Row-Level Security (Segurança em Nível de Linha - RLS).
Com o RLS ativado, as regras de autorização não ficam no código do aplicativo nem no prompt da IA: elas ficam gravadas no coração do motor do banco de dados.
- Toda vez que uma consulta é disparada, o PostgreSQL verifica qual é o
tenant_idautenticado na sessão; - O banco de dados filtra automaticamente as linhas antes mesmo de entregá-las ao aplicativo;
- Se um hacker ou agente de IA executar
SELECT * FROM contacts;, o banco só retornará os contatos pertencentes ao tenant daquela sessão. O vazamento de dados torna-se matematicamente impossível.
3. Script DDL Completo e Executável em SQL
Abaixo está o script de migração DDL de produção pronto para ser executado no editor SQL do Supabase. Ele cria a estrutura completa de dados necessária para sustentar um AI-First OS comercial com múltiplos clientes, auditoria de tokens e rastreamento de orçamentos.
-- ============================================================================
-- AI-FIRST OS: ESQUEMA DE PRODUÇÃO MULTITENANT BLINDADO COM RLS
-- PostgreSQL 16 / Supabase
-- ============================================================================
-- 1. Habilitação das extensões criptográficas e de identificadores únicos
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================================
-- TABELA 1: TENANTS (Empresas Clientes que contrataram a infraestrutura)
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
corporate_name VARCHAR(255) NOT NULL,
trade_name VARCHAR(255) NOT NULL,
tax_id VARCHAR(32) NOT NULL UNIQUE, -- CNPJ do cliente
is_active BOOLEAN DEFAULT TRUE NOT NULL,
plan_type VARCHAR(64) DEFAULT 'growth_retainer' NOT NULL,
monthly_budget_usd NUMERIC(10, 2) DEFAULT 100.00 NOT NULL,
created_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
-- ============================================================================
-- TABELA 2: CONTACTS (Leads e Clientes Finais atendidos pela IA no WhatsApp)
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.contacts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
whatsapp_e164 VARCHAR(32) NOT NULL, -- Formato: 5511999998888
full_name VARCHAR(255),
company_name VARCHAR(255),
qualification_score INT DEFAULT 0 CHECK (qualification_score BETWEEN 0 AND 100),
status VARCHAR(64) DEFAULT 'lead_novo' NOT NULL, -- lead_novo, cotando, proposta_enviada, cliente_ativo, inativo
lifetime_value_brl NUMERIC(12, 2) DEFAULT 0.00 NOT NULL,
custom_attributes JSONB DEFAULT '{}'::jsonb NOT NULL,
created_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
CONSTRAINT uq_tenant_whatsapp UNIQUE(tenant_id, whatsapp_e164)
);
-- ============================================================================
-- TABELA 3: CONVERSATIONS (Sessões de Diálogo)
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.conversations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
contact_id UUID NOT NULL REFERENCES public.contacts(id) ON DELETE CASCADE,
channel VARCHAR(32) DEFAULT 'whatsapp' NOT NULL,
handling_agent VARCHAR(64) DEFAULT 'ai_commercial_bot' NOT NULL, -- ai_commercial_bot, human_closer, paused
needs_human_attention BOOLEAN DEFAULT FALSE NOT NULL,
human_takeover_reason TEXT,
last_message_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
created_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
-- ============================================================================
-- TABELA 4: MESSAGES (Auditoria Completa de Mensagens, Áudios e Custos)
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
conversation_id UUID NOT NULL REFERENCES public.conversations(id) ON DELETE CASCADE,
direction VARCHAR(16) NOT NULL CHECK (direction IN ('inbound', 'outbound')),
modality VARCHAR(16) NOT NULL DEFAULT 'text' CHECK (modality IN ('text', 'audio', 'image', 'document')),
raw_content TEXT, -- Mensagem original ou URL do arquivo de áudio/foto
transcription TEXT, -- Transcrição gerada pelo Whisper (caso seja áudio)
model_used VARCHAR(64), -- Ex: 'claude-3-5-sonnet-20241022', 'gpt-4o'
tokens_input INT DEFAULT 0,
tokens_output INT DEFAULT 0,
cost_usd NUMERIC(8, 6) DEFAULT 0.000000, -- Custo exato da inferência
created_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
-- ============================================================================
-- TABELA 5: ORDERS_QUOTES (Orçamentos e Pedidos Comerciais)
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.orders_quotes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
contact_id UUID NOT NULL REFERENCES public.contacts(id) ON DELETE CASCADE,
quote_number VARCHAR(64) NOT NULL, -- Ex: 'COT-2026-0891'
status VARCHAR(32) DEFAULT 'draft' NOT NULL CHECK (status IN ('draft', 'sent_to_customer', 'approved', 'rejected', 'expired')),
total_amount_brl NUMERIC(12, 2) NOT NULL DEFAULT 0.00,
estimated_gross_margin_percent NUMERIC(5, 2),
line_items JSONB NOT NULL DEFAULT '[]'::jsonb, -- Array de itens: [{sku, desc, qty, unit_price, subtotal}]
expiration_date DATE NOT NULL,
created_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
updated_at TIMESTAMPTZ DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
CONSTRAINT uq_tenant_quote_number UNIQUE(tenant_id, quote_number)
);
-- ============================================================================
-- GATILHOS (TRIGGERS) PARA ATUALIZAÇÃO AUTOMÁTICA DE updated_at
-- ============================================================================
CREATE OR REPLACE FUNCTION public.handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = TIMEZONE('utc'::text, NOW());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_tenants_updated_at BEFORE UPDATE ON public.tenants FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER tr_contacts_updated_at BEFORE UPDATE ON public.contacts FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
CREATE TRIGGER tr_orders_quotes_updated_at BEFORE UPDATE ON public.orders_quotes FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
-- ============================================================================
-- ATIVAÇÃO DO ROW-LEVEL SECURITY (RLS) EM TODAS AS TABELAS
-- ============================================================================
ALTER TABLE public.tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders_quotes ENABLE ROW LEVEL SECURITY;
-- ============================================================================
-- POLÍTICAS DE RLS (ISOLAMENTO MULTITENANT POR SESSÃO)
-- ============================================================================
-- Criação de política que lê o tenant_id injetado nas variáveis de sessão da transação
-- Exemplo de injeção na aplicação: SET LOCAL app.current_tenant_id = 'uuid-do-cliente';
CREATE POLICY tenant_isolation_contacts ON public.contacts
FOR ALL
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
CREATE POLICY tenant_isolation_conversations ON public.conversations
FOR ALL
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
CREATE POLICY tenant_isolation_messages ON public.messages
FOR ALL
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
CREATE POLICY tenant_isolation_orders_quotes ON public.orders_quotes
FOR ALL
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
4. O Campo line_items JSONB: Flexibilidade de Catálogo B2B
Note a coluna line_items JSONB na tabela orders_quotes. Em empresas tradicionais (como distribuidoras de ferragens ou autopeças), um pedido de cotação não possui uma estrutura rígida de e-commerce com carrinho de compras.
O cliente pode enviar especificações técnicas personalizadas:
[
{
"sku": "TUB-PVC-100-NBR",
"description": "Tubo PVC Esgoto 100mm 6 metros Tigre",
"quantity": 25,
"unit_price_brl": 64.50,
"subtotal_brl": 1612.50,
"lead_time_days": 1
},
{
"sku": "CUR-PVC-100-90",
"description": "Curva 90 Graus Curta Esgoto 100mm",
"quantity": 10,
"unit_price_brl": 18.20,
"subtotal_brl": 182.00,
"lead_time_days": 1
}
]
O tipo nativo JSONB do PostgreSQL permite que você consulte, filtre e crie índices GIN diretamente sobre elementos dentro desse JSON sem precisar criar tabelas auxiliares complexas para orçamentos em fase de rascunho.
5. Exercício Prático de Fixação do Volume 4
- Simulação de Auditoria de Custo: Suponha que seu sistema atendeu um cliente que enviou 4 mensagens de áudio de 30 segundos cada e trocou 12 mensagens de texto com a IA. Se o Whisper custa US$ 0,006 por minuto de áudio e a LLM consumiu 8.000 tokens de entrada e 1.500 tokens de saída no Claude 3.5 Sonnet (custando US$ 3,00 por milhão de tokens de entrada e US$ 15,00 por milhão de tokens de saída):
- Qual foi o custo total em dólares dessa conversa inteira registrado na tabela
messages? - Converta esse valor para reais (considere US$ 1,00 = R$ 5,50).
- Compare esse custo de centavos com o salário de um atendente humano para triar a mesma demanda.
- Conceito de RLS: Se uma aplicação abrir uma conexão com o banco e esquecer de definir
app.current_tenant_id, o que acontecerá quando o comandoSELECT * FROM contacts;for disparado de acordo com as regras criadas acima?
No próximo volume (Volume 5), entraremos no universo da Engenharia Vetorial: vamos instalar e calibrar a extensão pgvector, aprender o que são vetores e embeddings sem matemática assustadora, configurar índices HNSW para alta escala e implementar o algoritmo de RAG Híbrido com Reciprocal Rank Fusion (RRF) escrito diretamente em SQL.