Documentação técnica da estrutura de dados, relacionamentos e padrões descobertos durante o desenvolvimento.
Tabela central que armazena perfis do Instagram que foram auditados.
CREATE TABLE instagram_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
full_name TEXT,
profile_pic_url TEXT,
followers_count INTEGER,
biography TEXT,
external_url TEXT,
is_verified BOOLEAN DEFAULT false,
is_business_account BOOLEAN DEFAULT false,
category TEXT,
posts_count INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);Registros: ~3 perfis atualmente Uso: Base de dados de todos os perfis analisados
Tabela separada para usuários do sistema (quem usa a ferramenta).
CREATE TABLE profiles (
profile_id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT,
full_name TEXT,
...outros campos de configuração do usuário...
);Registros: ~1 perfil atualmente Uso: Configurações e dados de quem usa a ferramenta (não dos perfis auditados)
Tabela gerenciada pelo Supabase Auth para login.
-- Tabela gerenciada automaticamente pelo Supabase
-- Não modificar diretamente
auth.users (
id UUID PRIMARY KEY,
email TEXT UNIQUE,
encrypted_password TEXT,
...
)Registros: Variável (usuários autenticados) Uso: Sistema de autenticação
Armazena os resultados completos das auditorias realizadas.
CREATE TABLE audits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES instagram_profiles(id) ON DELETE CASCADE,
score_overall INTEGER,
score_behavior INTEGER,
score_copy INTEGER,
score_offers INTEGER,
score_metrics INTEGER,
score_anomalies INTEGER,
classification TEXT,
analysis_json JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);FK: profile_id → instagram_profiles.id ✅
Uso: Resultado dos 5 auditores analisando um perfil
Armazena carrosséis e conteúdos gerados pelo Content Squad.
CREATE TABLE content_suggestions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
audit_id UUID NOT NULL REFERENCES audits(id) ON DELETE CASCADE,
profile_id UUID NOT NULL REFERENCES instagram_profiles(id) ON DELETE CASCADE,
content_json JSONB NOT NULL,
slides_json JSONB,
slides_v2_json JSONB,
reel_videos_json JSONB,
reels_json JSONB,
generated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);FKs:
audit_id → audits.id✅profile_id → instagram_profiles.id✅
Uso: Armazena carrosséis aprovados e seus slides visuais
┌─────────────────────┐
│ auth.users │ (Supabase Auth - login)
│ - id (PK) │
└──────────┬──────────┘
│
│ user_id (FK)
↓
┌─────────────────────┐
│ profiles │ (Usuários do Sistema)
│ - profile_id (PK) │
│ - user_id (FK) │
└─────────────────────┘
┌─────────────────────┐
│instagram_profiles │ (Perfis Auditados)
│ - id (PK) │
│ - username │
└──────────┬──────────┘
│
├── profile_id (FK)
│ ┌─────────────────────┐
│ │ audits │
└──→│ - id (PK) │
│ - profile_id (FK) │
└──────────┬──────────┘
│
├── audit_id (FK)
│ ┌─────────────────────────┐
│ │content_suggestions │
└──→│ - id (PK) │
│ - audit_id (FK) │
│ - profile_id (FK) │
└─────────────────────────┘
Erro no frontend ao fazer query com JOIN:
.from('content_suggestions')
.select(`
...,
instagram_profiles!left(username, full_name),
...
`)Erro retornado:
Could not find a relationship between 'content_suggestions'
and 'profiles' in the schema cache
- PostgREST mantém cache do schema para performance
- O cache não detectava automaticamente o FK
profile_id → instagram_profiles.id - Comandos como
NOTIFY pgrst, 'reload schema'não resolviam - O cache buscava relacionamento com tabela
profiles, nãoinstagram_profiles
-- ❌ Tentativa 1: Recriar FK
ALTER TABLE content_suggestions
DROP CONSTRAINT IF EXISTS content_suggestions_profile_id_fkey;
ALTER TABLE content_suggestions
ADD CONSTRAINT content_suggestions_instagram_profile_fkey
FOREIGN KEY (profile_id)
REFERENCES instagram_profiles(id)
ON DELETE CASCADE;
-- ❌ Tentativa 2: Forçar reload do schema
NOTIFY pgrst, 'reload schema';
NOTIFY pgrst, 'reload config';
-- ❌ Tentativa 3: Reiniciar serviços Supabase
-- (Via dashboard do Supabase)Resultado: FK criada com sucesso no banco, mas erro persistia no frontend.
Criamos uma VIEW que faz o JOIN diretamente no SQL, contornando o cache do PostgREST.
-- Criar VIEW que já retorna dados JOINados
CREATE VIEW content_suggestions_with_profile AS
SELECT
cs.id,
cs.audit_id,
cs.profile_id,
cs.content_json,
cs.generated_at,
cs.created_at,
cs.updated_at,
cs.slides_json,
cs.reels_json,
cs.slides_v2_json,
cs.reel_videos_json,
jsonb_build_object(
'id', ip.id,
'username', ip.username,
'full_name', ip.full_name,
'profile_pic_url', ip.profile_pic_url,
'followers_count', ip.followers_count
) as instagram_profiles,
jsonb_build_object(
'id', a.id,
'classification', a.classification,
'score_overall', a.score_overall
) as audits
FROM content_suggestions cs
LEFT JOIN instagram_profiles ip ON cs.profile_id = ip.id
LEFT JOIN audits a ON cs.audit_id = a.id;
-- Dar permissões
GRANT SELECT ON content_suggestions_with_profile TO anon, authenticated, service_role;// ❌ ANTES: Query com JOIN (não funcionava)
const { data, error } = await supabase
.from('content_suggestions')
.select(`
*,
instagram_profiles!left(username, full_name),
audits!left(classification)
`)
.order('generated_at', { ascending: false })
// Processar dados manualmente
const processedData = data?.map(item => ({
...item,
instagram_profiles: item.instagram_profiles || {},
audits: item.audits || {}
}))// ✅ DEPOIS: Query direto na VIEW (funciona!)
const { data, error } = await supabase
.from('content_suggestions_with_profile')
.select('*')
.order('generated_at', { ascending: false })
// VIEW já retorna dados no formato correto
setContents(data || [])- ✅ Contorna o cache do PostgREST - JOIN acontece no SQL
- ✅ Performance melhor - Banco faz JOIN otimizado
- ✅ Código mais limpo - Não precisa processar manualmente
- ✅ Type-safe - TypeScript infere o tipo correto
- ✅ Consistente - Sempre retorna mesmo formato
❌ NÃO FAZER ASSIM (pode dar erro de cache):
.from('tabela_principal')
.select(`
*,
tabela_relacionada!left(campo1, campo2)
`)✅ FAZER ASSIM (usar VIEW):
-- 1. Criar VIEW no Supabase
CREATE VIEW minha_view AS
SELECT
t.*,
jsonb_build_object(
'campo1', r.campo1,
'campo2', r.campo2
) as tabela_relacionada
FROM tabela_principal t
LEFT JOIN tabela_relacionada r ON t.fk_id = r.id;
GRANT SELECT ON minha_view TO anon, authenticated, service_role;// 2. Usar VIEW no código
.from('minha_view')
.select('*')SELECT
tc.constraint_name,
kcu.column_name,
ccu.table_name AS foreign_table,
ccu.column_name AS foreign_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON tc.constraint_name = ccu.constraint_name
WHERE tc.table_name = 'content_suggestions'
AND tc.constraint_type = 'FOREIGN KEY';-- Verificar se existem content_suggestions sem profile válido
SELECT
cs.id,
cs.profile_id,
CASE
WHEN ip.id IS NULL THEN '❌ ÓRFÃO'
ELSE '✅ OK'
END as status
FROM content_suggestions cs
LEFT JOIN instagram_profiles ip ON cs.profile_id = ip.id;DELETE FROM content_suggestions
WHERE profile_id NOT IN (
SELECT id FROM instagram_profiles
);| Arquivo | Descrição |
|---|---|
database/optimized-schema.sql |
Schema completo do banco |
database/FIX-CONTENT-SUGGESTIONS-FK.sql |
Script de correção de FKs |
database/FIX-FK-SIMPLE.sql |
Script simplificado de verificação |
scripts/diagnose-supabase-schema.js |
Script Node.js de diagnóstico |
app/dashboard/bau/page.tsx |
Página que usa a VIEW |
O PostgREST (usado pelo Supabase) mantém cache agressivo do schema. Mesmo com FKs corretas, pode não detectar relacionamentos automaticamente.
Para relacionamentos complexos ou problemáticos, VIEWs resolvem definitivamente porque o JOIN acontece no banco antes do PostgREST processar.
# Ordem correta:
1. Verificar órfãos (SELECT com LEFT JOIN)
2. Deletar órfãos (DELETE WHERE NOT IN)
3. Criar FK (ALTER TABLE ADD CONSTRAINT)
4. Verificar sucesso (SELECT constraint_name)Se uma FK aponta para instagram_profiles, o PostgREST pode buscar profiles no cache. Usar VIEWs evita essa confusão.
| Tabela | Registros | Status |
|---|---|---|
instagram_profiles |
3 | ✅ OK |
profiles |
1 | ✅ OK |
audits |
~15 | ✅ OK |
content_suggestions |
~10 | ✅ OK (sem órfãos) |
content_suggestions_with_profile (VIEW) |
~10 | ✅ Funcionando |
content_suggestions.audit_id → audits.id ✅
content_suggestions.profile_id → instagram_profiles.id ✅
audits.profile_id → instagram_profiles.id ✅
profiles.user_id → auth.users.id ✅
- Criar VIEWs similares para outras páginas se necessário
- Monitorar performance das VIEWs em produção
- Adicionar índices se queries ficarem lentas
- Documentar novos padrões conforme o sistema cresce
Última atualização: 2026-02-26 Autor: Claude Code Versão: 1.0