Arquitetura de dados na Nexus G: como modelei tabelas e segurança no Supabase/Postgres

@wendellnuneslima
Fechando o tripé técnico do pilar de Engenharia depois da stack geral e do SEO técnico com Next.js falta a camada que sustenta tudo por trás: como os dados de perfil, conteúdo e depoimento se relacionam no Postgres, e como o Supabase resolve, com Row Level Security, o problema de cada usuário só poder mexer no que é dele.
O primeiro passo foi desenhar o modelo relacional pensando na pergunta certa: o que pertence a quem, e o que depende do quê. Na Nexus G, um perfil é a entidade central, e conteúdo, depoimento e visualização existem apenas em função de um perfil nenhuma dessas tabelas faz sentido isolada. O esqueleto simplificado fica assim:
create table profiles (
id uuid primary key references auth.users(id),
username text unique not null,
name text not null,
role text,
bio text,
avatar_url text,
created_at timestamptz default now()
);
create table contents (
id uuid primary key default gen_random_uuid(),
profile_id uuid references profiles(id) on delete cascade,
title text not null,
slug text not null,
body text not null,
category text,
published boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now(),
unique (profile_id, slug)
);
create table testimonials (
id uuid primary key default gen_random_uuid(),
profile_id uuid references profiles(id) on delete cascade,
author_name text not null,
message text not null,
created_at timestamptz default now()
);
create table content_views (
id uuid primary key default gen_random_uuid(),
content_id uuid references contents(id) on delete cascade,
viewed_at timestamptz default now(),
visitor_hash text
);
Um detalhe que fez diferença real: profiles.id referencia diretamente auth.users(id). Em vez de manter uma tabela de usuários separada da tabela de autenticação, o perfil é uma extensão direta do usuário autenticado do Supabase. Isso elimina uma camada inteira de sincronização manual entre "quem fez login" e "quem é dono de qual perfil" a própria chave estrangeira já resolve essa ligação.
A tabela content_views merece atenção à parte porque registrar visualização é diferente de registrar conteúdo. Em vez de incrementar um contador direto na tabela contents a cada view o que geraria concorrência de escrita toda vez que dois visitantes acessam o mesmo perfil ao mesmo tempo cada visualização vira uma linha própria, e a contagem exibida no painel é uma agregação:
select content_id, count(*) as total_views
from content_views
group by content_id;
Isso separa claramente o dado bruto (cada evento de visualização, que pode alimentar análises futuras como visualização por período) da métrica exibida (o total simples que aparece no perfil), sem travar a tabela principal de conteúdo com escrita constante.
O segundo pilar é a segurança, resolvida com Row Level Security ativado em cada tabela. O RLS do Postgres nega acesso por padrão assim que é habilitado, e cada operação leitura, escrita, atualização precisa de uma política explícita. Para perfis, qualquer visitante pode ler qualquer perfil público, mas só o próprio dono pode editar o seu:
alter table profiles enable row level security;
create policy "Perfis são públicos para leitura"
on profiles for select
using (true);
create policy "Usuário só edita o próprio perfil"
on profiles for update
using (auth.uid() = id);
Para conteúdo, a regra muda um pouco: leitura pública só vale para o que está marcado como publicado, e escrita só vale para o dono:
alter table contents enable row level security;
create policy "Conteúdo publicado é público"
on contents for select
using (published = true);
create policy "Dono pode gerenciar seus próprios conteúdos"
on contents for all
using (auth.uid() = profile_id);
Essa segunda política junta select, insert, update e delete numa única regra porque o critério de dono é o mesmo pras quatro operações e é justamente essa política que garante que, mesmo se algum bug de front-end tentasse expor um formulário de edição pro usuário errado, o banco recusaria a operação antes de qualquer dado sensível trafegar. A segurança não depende só do que a interface esconde, depende do que o banco realmente permite.
Um ponto de atenção prático: RLS mal configurado é silencioso ele não gera erro visível, simplesmente retorna zero linhas quando a política nega acesso. Isso torna testar cada política manualmente, simulando o auth.uid() de diferentes usuários, uma etapa que não dá pra pular antes de subir qualquer tabela nova pra produção.
Com esse modelo perfil como entidade central ligada direto à autenticação, conteúdo e depoimento dependentes por chave estrangeira, visualização tratada como evento agregável, e RLS garantindo que cada política de acesso viva dentro do próprio banco a Nexus G sustenta múltiplos perfis crescendo em paralelo sem que a arquitetura vire gargalo. É a parte que ninguém vê no perfil público, mas que decide se a plataforma aguenta escalar ou não.
Comentários
Mais de Wendell Nunes Lima

Escrito por
Wendell Nunes LimaFundador e Desenvolvedor da Nexus G
Especialista em desenvolvimento de plataformas digitais e fundador da Nexus G, um ecossistema criado para fortalecer a autoridade profissional com conteúdo otimizado para SEO e páginas preparadas para ranquear no Google.


