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.
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_1…attribute_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 edgecreate-surveyinsere todos os respondentes em lote junto com a pesquisa. Fluxo em Pesquisas — Capítulo 05. -
Edição posterior —
EditSurveyClient→ edgeedit-survey. O modo full permite alterar toda a lista; o modo limited só aceita mudança deemailetelefonede respondentes existentes. Tabela de modos no mesmo capítulo de Pesquisas.
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).
Modelo de dados
Schema confirmado via Supabase MCP em 2026-05-21. Tabela única, wide, sem normalização de atributos.
| Coluna | Tipo | Nullability / default |
|---|---|---|
id | bigint | PK, nextval() |
survey_id | numeric | NOT NULL — sem FK declarada (ver Cap. 06) |
name | varchar | NOT NULL |
email | varchar | nullable |
phone | varchar(14) | nullable |
attribute_1 | varchar | NOT NULL |
attribute_2 … attribute_7 | varchar | nullable |
email_sent | smallint | NOT NULL default 0 |
whatsapp_sent | smallint | nullable, default 0 |
finished_filling | timestamptz | nullable |
active | smallint | NOT NULL default 1 |
created_at, updated_at | timestamptz | NOT 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_id | CASCADE |
mydb.survey_message_respondents.respondent_id | CASCADE |
mydb.survey_responses.respondent_id e evaluatee_id | Sem cascade — limpeza aplicativa (Cap. 06) |
Tabela pareada de atributos
mydb.survey_attributes guarda os nomes dos
atributos por pesquisa (attribute_1…attribute_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.
As colunas attribute_1…attribute_7 seguem o
mesmo padrão wide das perguntas (question_1…question_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.
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:
-- 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.
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.
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:
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).
Pegadinhas
Sem UNIQUE em (survey_id, email) nem em (survey_id, phone)
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
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:
DELETE FROM survey_responses WHERE respondent_id = ?DELETE FROM survey_responses WHERE evaluatee_id = ?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.