Neuroredes
Módulos do Produto

Respondentes

A entidade mais central depois de Pesquisas: cada linha em mydb.survey_respondents é uma pessoa que avalia todos os outros respondentes da mesma pesquisa e é avaliada por todos eles. Os outros módulos do produto (envio, resposta pública, dashboard, grafo, relatórios) leem ou escrevem nessa tabela.

Capítulo 01

Papel no produto

O modelo “todos avaliam todos” é introduzido em Pesquisas — Capítulo 01: numa pesquisa com N respondentes existem até N × (N − 1) pares avaliador/avaliado, cada um gerando uma linha em mydb.survey_responses com até cinco notas. Respondente é o lado humano desse modelo.

Cada respondente carrega ainda até sete atributos categorizadores (departamento, cargo, senioridade, etc.). Os nomes dos atributos vivem em mydb.survey_attributes — por pesquisa, decididos na criação. Os valores vivem em mydb.survey_respondents.attribute_1attribute_7. O pareamento entre as duas tabelas é posicional, sem chave compartilhada além da ordem das colunas (ver Capítulo 06).

Onde a gestão acontece hoje

Não há tela, rota ou modal dedicado a “Gerenciar respondentes” no produto. A manipulação acontece nos extremos do ciclo de vida da pesquisa:

  • Cadastro inicial — passo 4 do stepper de criação (CreateSurveyClient). A edge create-survey insere todos os respondentes em lote junto com a pesquisa. Fluxo em Pesquisas — Capítulo 05.
  • Edição posteriorEditSurveyClient → edge edit-survey. O modo full permite alterar toda a lista; o modo limited só aceita mudança de email e telefone de respondentes existentes. Tabela de modos no mesmo capítulo de Pesquisas.
Nota

Existem no repositório uma rota app/(main)/surveys/[id]/respondents/page.tsx, um componente components/ManageRespondents/* e as edges create-respondent, update-respondent, delete-respondent e get-respondents deployadas. Todas estão dormentes: a rota termina em redirect('/surveys'), o componente é importado apenas em linha comentada, e nenhuma edge tem caller no app, components, services, rpc ou lib. É decisão de produto — não estão sendo reativadas. Esta página documenta o que está em uso.

O respondente como ator — a pessoa que recebe o link e preenche o formulário sem login — pertence a Resposta Pública. O canal de chegada do link está em Envio (Magic Links).

Capítulo 02

Modelo de dados

Schema confirmado via Supabase MCP em 2026-05-21. Tabela única, wide, sem normalização de atributos.

Coluna Tipo Nullability / default
idbigintPK, nextval()
survey_idnumericNOT NULL — sem FK declarada (ver Cap. 06)
namevarcharNOT NULL
emailvarcharnullable
phonevarchar(14)nullable
attribute_1varcharNOT NULL
attribute_2attribute_7varcharnullable
email_sentsmallintNOT NULL default 0
whatsapp_sentsmallintnullable, default 0
finished_fillingtimestamptznullable
activesmallintNOT NULL default 1
created_at, updated_attimestamptzNOT NULL default CURRENT_TIMESTAMP

Constraints no banco: apenas a PK em id. Nenhuma unique constraint em (survey_id, email) ou (survey_id, phone); nenhuma foreign key declarada para mydb.surveys. Os dois pontos viram pegadinha no Capítulo 06.

FKs entrantes

Três tabelas referenciam mydb.survey_respondents(id); apenas duas têm cascade automática:

FK ON DELETE
mydb.survey_respondent_tokens.respondent_idCASCADE
mydb.survey_message_respondents.respondent_idCASCADE
mydb.survey_responses.respondent_id e evaluatee_idSem cascade — limpeza aplicativa (Cap. 06)

Tabela pareada de atributos

mydb.survey_attributes guarda os nomes dos atributos por pesquisa (attribute_1attribute_7, PK em survey_id). mydb.survey_respondents guarda os valores nas colunas homônimas. A chave conceitual que liga “Engenharia” em uma linha de respondente ao rótulo “Departamento” na pesquisa é puramente o índice da coluna. Cabe ao módulo de pesquisa cuidar dos rótulos — Pesquisas — Capítulo 02.

Nota

As colunas attribute_1attribute_7 seguem o mesmo padrão wide das perguntas (question_1question_5 em mydb.surveys). Limite rígido de sete atributos: subir esse teto exige migration nas duas tabelas. O argumento longo está em Pesquisas — Capítulo 02.

Capítulo 03

Permissões e RLS

Como toda manipulação de respondente passa pelo módulo de pesquisa, as permissões são as mesmas: ADMIN e SURVEYOR podem cadastrar e editar (via create-survey e edit-survey), VIEWER não. O SurveysCard esconde os botões “Editar” e “Disparar” para VIEWER. ADMIN com profile.company_id = null usa o survey.empresa_id da pesquisa-alvo (padrão de Autenticação — Capítulo 02).

RLS em mydb.survey_respondents está habilitada, com duas policies confirmadas via MCP:

policies em mydb.survey_respondents sql
-- Leitura: qualquer usuário autenticado
"Authenticated users can view survey_respondents"
  for select to public
  using ((select auth.role()) = 'authenticated');

-- Escrita: apenas service_role
"Service role can manage survey_respondents"
  for all to service_role
  using (true) with check (true);

A policy de SELECT não escopo por empresa nem por pesquisa — qualquer login lê a tabela inteira via supabase.schema('mydb').from('survey_respondents'). O produto não usa esse caminho hoje, mas a superfície existe. Mesma pegadinha de leitura aberta a authenticated já está catalogada em Backend — Capítulo 08 e nos “Alertas de segurança pendentes” do ROADMAP.md.

Capítulo 04

Estados do respondente

Quatro flags acompanham cada respondente. Nenhuma é escrita pelo próprio módulo: cada uma pertence a um fluxo de outro módulo.

Flag Tipo Quem escreve
active smallint (0/1) Inicializado em 1 por create-survey. Atualizável por update-respondent, que não tem caller hoje — em produção todas as linhas estão com active = 1.
email_sent smallint (0/1) Marcado pelos envios em massa (magic-link-send-bulk), indiretamente pela RPC survey_message_respondents_update_status. Resetado para 0 pelo edit-survey destrutivo. Ver Envio (Magic Links).
whatsapp_sent smallint (0/1) Atualizado pelo consumer Lambda bulk-whatsapp-consumer após confirmação da Meta API (ver Lambdas — Capítulo 08). Resetado pelo edit-survey destrutivo.
finished_filling timestamptz Setado pela edge save-responses com new Date().toISOString() quando o respondente avaliou todos os outros (responsesCount >= totalParticipantsToEvaluate). Não escrito em saves parciais. Resetado para NULL pelo edit-survey destrutivo. Ver Resposta Pública.

Distribuição em produção (Supabase MCP, 2026-05-21): 1574 respondentes ativos, 229 com email_sent = 1, 0 com whatsapp_sent = 1, 71 com finished_filling IS NOT NULL. O zero em WhatsApp é compatível com a hipótese de que a propagação do flag pelo consumer Lambda não está chegando ao banco — diagnóstico fica em Envio e Lambdas — Capítulo 08.

Nota em passing: a edge dormente get-respondents deriva email_sent da união da coluna direta com mydb.survey_message_respondents (canal email, status sent), “OR-ando” as duas fontes. Como ninguém chama essa edge hoje, a derivação não tem efeito prático.

Capítulo 05

Integração com outros módulos

Pesquisa

Único caminho ativo de criação e mutação. create-survey insere respondentes em lote no fim do stepper (telefones limpos para conter só dígitos, máx. 14). edit-survey aceita a lista completa em modo full; em modo limited, só respondentContacts (apenas email e telefone). Sanitização do telefone fica em supabase/functions/edit-survey/index.ts:

edit-survey/index.ts typescript
let cleanPhone = null;
if (respondente.telefone) {
  cleanPhone = respondente.telefone.replace(/\D/g, '').substring(0, 14);
}

Editar uma pesquisa que já disparou magic links exige confirmDestructive: true: a edge revoga tokens, apaga respostas e reseta os três flags (email_sent = 0, whatsapp_sent = 0, finished_filling = NULL) antes de aplicar a edição. Fluxo completo em Pesquisas — Capítulo 05.

Envio (Magic Links)

magic-link-send-bulk lê respondentes com active = 1 e email não-nulo, gera tokens, envia via SMTP e atualiza status na tabela join mydb.survey_message_respondents. whatsapp-send-bulk faz o mesmo filtro com phone não-nulo, mas enfileira em SQS e delega o envio ao consumer Lambda. Detalhes em Envio (Magic Links) e Lambdas (AWS).

Resposta Pública

Cada token em mydb.survey_respondent_tokens aponta para um respondente via respondent_id (FK com ON DELETE CASCADE). A edge magic-link-validate confere hash, expiração e revogação antes de liberar o formulário; save-responses grava cada par avaliador/avaliado em mydb.survey_responses e marca finished_filling quando o respondente completa a rodada. Ver Resposta Pública e Autenticação — Capítulo 07.

Análise (Dashboard, Grafo, Relatórios)

As três Lambdas HTTP leem mydb.survey_respondents direto: respondentes viram nós do grafo (com tamanho e cor derivados dos atributos), o dashboard usa count(finished_filling IS NOT NULL) como denominador de progresso, e o Excel exporta nome, email, atributos e flags. Dashboard, Gráfico e Relatórios (Excel).

Capítulo 06

Pegadinhas

Sem UNIQUE em (survey_id, email) nem em (survey_id, phone)

Atenção

O banco aceita duplicatas, e elas existem em produção. Identificado via MCP em 2026-05-21: 50 respondentes com email@mail.com em survey_id = 11 (placeholders), 37 com o mesmo email em survey_id = 9 e survey_id = 10, e casos com 2–3 cópias do mesmo email em pesquisas reais (survey_id = 17, survey_id = 47). O stepper de criação também não checa unicidade no cliente. Consequência prática: dois respondentes “diferentes” com o mesmo contato recebem dois magic links distintos para a mesma pesquisa e contam como dois nós separados no grafo.

Sem FK declarada para mydb.surveys

Atenção

A coluna survey_id é apenas numeric NOT NULL — não há foreign key no pg_constraint. A integridade referencial é puramente aplicativa: create-respondent (dormente) faz SELECT id FROM surveys WHERE id = ? antes de inserir, mas qualquer escrita direta no banco bypassa a checagem. Apagar uma pesquisa direto via SQL (sem passar pelo delete-survey, que cuida da cascata manual) deixa respondentes órfãos no schema.

Cascata mista na exclusão

Quando uma linha de survey_respondents é apagada, o banco cascateia automaticamente para survey_respondent_tokens e survey_message_respondents (ambas com ON DELETE CASCADE). Mas mydb.survey_responses tem PK composta (respondent_id, evaluatee_id) sem cascade — a edge dormente delete-respondent limpa em três passos:

  1. DELETE FROM survey_responses WHERE respondent_id = ?
  2. DELETE FROM survey_responses WHERE evaluatee_id = ?
  3. DELETE FROM survey_respondents WHERE id = ? (e o banco cascateia tokens/messages)

Quem replicar esse caminho (RPC futura, script de cleanup) precisa respeitar a ordem e fazer as duas deleções aplicativas de survey_responses primeiro. Caso contrário, o respondente e seus tokens somem mas as respostas que ele deu (ou recebeu) ficam órfãs no banco.

Pareamento de atributos é posicional

mydb.survey_attributes.attribute_3 guarda o rótulo (“Departamento”); mydb.survey_respondents.attribute_3 guarda o valor (“Engenharia”). A única chave que liga os dois é o índice da coluna. Mexer na ordem em uma só das duas tabelas — por exemplo, uma migration que desloca atributos para fechar buracos — desalinha silenciosamente o significado de todos os respondentes da pesquisa, sem erro de constraint.

Cadastro em lote só pelo stepper de criação

Não há rota, modal ou componente standalone para importar respondentes de CSV/Excel ou colar uma lista. O único caminho de bulk insert é o passo 4 do stepper de CreateSurveyClient no momento da criação da pesquisa. Para adicionar respondentes a uma pesquisa já criada, a alternativa é EditSurveyClient → edge edit-survey, sujeita à matriz de modos full/limited/readonly descrita em Pesquisas — Capítulo 05.