Neuroredes
Plataforma

Administradores de Pesquisa

CRUD dos usuários com papel SURVEYOR em mydb.surveyors, e o caminho de convite por email que cria a identidade em auth.users e leva o convidado a definir senha em /confirmar-convite. ADMIN cria, edita e exclui; SURVEYOR enxerga e edita apenas a própria empresa; VIEWER não chega à rota.

Capítulo 01

Onde mora o módulo

A tela vive em app/(main)/surveyors/, sob o route group com header global. O Server Component faz o gate de papel — VIEWER é redirecionado direto para /surveys. ADMIN e SURVEYOR chegam à mesma página, mas com listagens distintas (ver Cap. 05). A criação de SURVEYOR é o ponto onde este módulo se acopla com auth.html Cap. 04: envolve auth.admin.inviteUserByEmail e gravação em duas tabelas (auth.users + mydb.surveyors).

Arquivo Responsabilidade
app/(main)/surveyors/page.tsx Server Component. Lê cookie user_profile, redireciona VIEWER, chama getSurveyorsRpc(profile) e getCompaniesRpc(profile) (ambos envolvidos em cache() do React) e injeta o resultado em SurveyorsClient.
app/(main)/surveyors/actions.ts Server Actions createSurveyorAction, updateSurveyorAction, deleteSurveyorAction. Cada uma re-roda o Zod e chama o service. create revalida papel via fetchUserProfileFromEdge(); update aceita isSelfUpdate e dispara logout() quando o usuário muda o próprio email.
components/Surveyors/SurveyorsClient/SurveyorsClient.tsx Layout em dois painéis. Lista de cards à esquerda (busca + paginação 10/pág, com sort que coloca o usuário atual no topo); modal lateral de criação/edição à direita. Renderiza ConfirmDialog para exclusão.
components/Surveyors/SurveyorsCard/SurveyorsCard.tsx Card individual. Mostra nome, empresa, email e telefone. Para ADMIN, exibe um ícone de sincronização (isSynced = !!supabase_auth_id) com tooltip. Lixeira aparece para ADMIN sempre, e para SURVEYOR quando o card não é dele.
components/Surveyors/CreateSurveyorForm/CreateSurveyorForm.tsx Formulário de criação. Auto-seleciona a primeira empresa do dropdown. Mostra um ConfirmDialog avisando que será disparado email antes de chamar a action. Select de empresa disabled quando o caller é SURVEYOR.
components/Surveyors/UpdateSurveyorForm/UpdateSurveyorForm.tsx Formulário de edição. Aplica formatPhone ao carregar. Marca isSelfUpdate quando o email do card bate com currentUserEmail.
rpc/surveyors/get-surveyors.ts Camada RPC. getSurveyorsRpc(profile) despacha por papel: ADMIN → get_surveyors_paginated, SURVEYOR → get_surveyors_by_company. mapRow normaliza null para string vazia em campos opcionais.
services/surveyors.service.ts Service que invoca edges. createSurveyor chama create-user-surveyor; updateSurveyor e deleteSurveyor chamam surveyors-crud/{id}. A função getSurveyors (POST na surveyors-crud) está definida mas não tem caller real — substituída pela RPC.
supabase/functions/surveyors-crud/index.ts Vive no repo desde 2026-06-10 (antes só existia deployada). verify_jwt: true; deriva identidade do JWT via _shared/auth.ts, com autorização deny-by-default (ADMIN, ou SURVEYOR da própria empresa). Atende PUT/DELETE (e GET/POST dormentes).
supabase/functions/create-user-surveyor/index.ts Vive no repo desde 2026-06-10 (antes só existia deployada; ver auth.html Cap. 08). verify_jwt: true; identidade do JWT e gate ADMIN-only via _shared/auth.ts. Cria o convite e a linha em mydb.surveyors.
supabase/functions/create-full-user/ Antecessora versionada no repo. Sem caller real em app, components, services, rpc ou lib (grep vazio em 2026-05-22) — código vestigial.
lib/zod/surveyor.validation.ts surveyorValidationSchema e o tipo derivado SurveyorFormData. Ver Cap. 04.
types/surveyor.type.ts Surveyor, CreateSurveyorPayload, UpdateSurveyorPayload, PaginatedSurveyorsResponse.
app/(auth)/confirmar-convite/page.tsx Página de definição de senha onde o convite termina. Documentada em auth.html Cap. 04; este módulo só dispara o email.
mydb.surveyors Tabela física. 11 colunas, PK id bigint serial, UNIQUE em email via índice Laravel legado. Ver Cap. 02.
Capítulo 02

Modelo de dados

A tabela mydb.surveyors tem 11 colunas. Os campos marcados NOT NULL são exigidos no INSERT; a Zod e o formulário tratam a obrigatoriedade antes de chegar ao Postgres. active e password têm default e receberam tratamento especial das edges — ver pegadinhas.

id
bigint, PK. Default nextval('mydb.surveyors_id_seq'). É o surveyor_id usado pelo frontend; nenhuma outra tabela do schema declara FK para ele (ver Cap. 09).
name
varchar, NOT NULL. Exibido no card como título.
phone
varchar, nullable. A UI aplica formatPhone apenas para exibição; o que é gravado é a string formatada do input.
email
varchar, NOT NULL. Coberto por UNIQUE index idx_30703_surveyors_email_unique.
password
varchar, NOT NULL. Coluna legada que não autentica. A edge create-user-surveyor grava o literal 'remover-essa-coluna'; a senha real vive em auth.users e é atualizada por supabase.auth.updateUser({ password }) na rota /confirmar-convite.
active
smallint, NOT NULL, default 1. A criação grava sempre 1. Nenhum lugar do frontend lê o valor para mudar UI ou comportamento.
company_id
numeric, NOT NULL. Chave do escopo de pesquisa do SURVEYOR. Tem índice idx_30703_surveyors_company_id_foreign (nome herdado do Laravel sugerindo FK) — mas a foreign key correspondente nunca foi declarada.
email_verified_at
timestamptz, nullable. Coluna legada do Laravel. Sem leitura no frontend; a confirmação de email real é auth.users.email_confirmed_at, consultado pela edge de convite.
created_at, updated_at
timestamptz, NOT NULL, default CURRENT_TIMESTAMP. updated_at é atualizado pela RPC update_surveyor_profile com NOW(); sem trigger.
supabase_auth_id
uuid, nullable. FK declarada fk_supabase_auth_id para auth.users(id) com ON UPDATE CASCADE ON DELETE SET NULL. É a ponte que o get-profile usa para derivar o papel (ver auth.html Cap. 02).
Nota

O UNIQUE em email aparece como índice (idx_30703_surveyors_email_unique), não como constraint nomeada em pg_constraint. É o mesmo padrão Laravel legacy de mydb.companies. Colisão devolve 23505 do Postgres; a edge create-user-surveyor entrega o erro como detalhe, e a Server Action o reescreve para "Erro ao criar pesquisador. Verifique se o e-mail é novo e nunca foi utilizado." quando detecta duplicate key ou non-2xx status code.

População atual

6 surveyors em produção (consulta MCP em 2026-05-22) distribuídos em 4 empresas distintas. Todos têm supabase_auth_id populado. Cinco deles têm a coluna password com o literal 'remover-essa-coluna'; uma linha (id=4, a conta original do projeto) ainda carrega o hash bcrypt remanescente da migração Laravel — caso isolado, não usado para autenticar.

Capítulo 03

Composição da tela

SurveyorsClient monta um layout em dois painéis. O esquerdo é a lista de administradores; o direito é o modal contextual que aparece em modo editing (clique em card) ou creating (botão "Adicionar"). O modo idle esconde o painel direito. Não há filtro de data — apenas busca textual.

Elemento O que faz
SearchFilterBar Campo de busca livre. Filtra por name, email ou nome da empresa (companiesMap.get(s.company_id)) com toLowerCase().includes(query).
Lista de cards filtered.sort(...) coloca o usuário atual (currentUserEmail) no topo; demais entram pela ordem que a RPC devolveu. Paginação de 10 itens via usePagination. Empty state com EmptyState + ícone FiUsers quando não há resultado.
SurveyorsCard Título (name), nome da empresa via companiesMap, email e (opcionalmente) telefone. Para ADMIN exibe ainda um ícone FiCheckCircle/FiAlertTriangle ligado a !!supabase_auth_id.
Pagination Numerada, label "administradores".
Botão "Adicionar novo administrador de pesquisa" Renderizado apenas quando role === 'ADMIN'. Abre CreateSurveyorForm.
CreateSurveyorForm Nome, organização (select), email e telefone (com máscara formatPhone). Antes de chamar a action, mostra um ConfirmDialog avisando que será disparado um email para o endereço informado. Select de empresa disabled para SURVEYOR.
UpdateSurveyorForm Mesmos campos. isSelfUpdate é deduzido em tempo de submit (surveyor?.email === currentUserEmail) e propagado para a action.
ConfirmDialog (excluir) "Tem certeza que deseja excluir X? Esta ação é irreversível." Não cita cascata de pesquisas porque não há cascata — ver Cap. 09.
Atenção

O ícone de sincronização que ADMIN vê no card tem dois textos (tooltips) curiosos: o estado "sincronizado" confirma que existe linha em mydb e em auth.users; o estado "não sincronizado" instrui literalmente “Cadastre-o manualmente pelo painel Supabase”. Isso referencia o cenário em que a linha de mydb.surveyors existe mas supabase_auth_id é NULL — herança da migração Laravel, ou consequência da FK fk_supabase_auth_id com ON DELETE SET NULL quando o auth.users some por fora. Hoje todos os 6 surveyors em produção estão sincronizados; o estado de alerta é, na prática, um lembrete histórico.

Capítulo 04

Validação (Zod)

surveyorValidationSchema em lib/zod/surveyor.validation.ts é a única camada semântica do client. As edges (surveyors-crud e create-user-surveyor) só checam “campos obrigatórios” de forma rasa; as RPCs do banco validam apenas o tipo da coluna.

lib/zod/surveyor.validation.ts typescript
export const surveyorValidationSchema = z.object({
  name: z.string().min(3, 'O nome deve ter pelo menos 3 caracteres.'),
  email: z.email('E-mail inválido.'),
  phone: z.string().optional().or(z.literal('')),
  company_id: z.coerce.number().min(1, 'A organização é obrigátoria.'),
});

export type SurveyorFormData = z.infer<typeof surveyorValidationSchema>;

Três campos efetivamente obrigatórios: name (mínimo 3), email (formato), company_id (coerce e min(1)). phone aceita string vazia ou undefined — sem regex, sem comprimento mínimo. A máscara visual em formatPhone não chega a impor formato ao banco; o que for digitado entra como string em mydb.surveyors.phone.

Atenção

A Zod não valida unicidade de email. A descoberta de colisão acontece só quando o INSERT atinge o índice idx_30703_surveyors_email_unique e o Postgres devolve 23505. A Server Action detecta as substrings duplicate key ou non-2xx status code no erro retornado pela edge e troca por uma mensagem em português: “Erro ao criar pesquisador. Verifique se o e-mail é novo e nunca foi utilizado.” Qualquer outra forma de erro chega crua no toast.

Capítulo 05

Fluxos CRUD

Listagem, edição e exclusão. A criação tem fluxo próprio (envio de email, gravação em duas tabelas, rollback) e está no Cap. 06.

Listar

Server Component em page.tsx lê o cookie user_profile. VIEWER é redirecionado para /surveys. ADMIN e SURVEYOR seguem para getSurveyorsRpc(profile), que despacha por papel:

  • ADMINsupabase.rpc('get_surveyors_paginated', { p_schema: 'mydb', p_search: null, p_page: 1, p_per_page: 100 }). Devolve até 100 administradores ordenados pela RPC.
  • SURVEYOR com profile.company_id definido → supabase.rpc('get_surveyors_by_company', { p_schema: 'mydb', p_company_id, p_search: null, p_page: 1, p_per_page: 100 }).
  • SURVEYOR sem company_id ou qualquer outro caso → array vazio.

O fetch é envolvido em cache() do React para deduplicar chamadas concomitantes dentro do mesmo request. mapRow normaliza null para string vazia em phone, email_verified_at e supabase_auth_id. O dropdown de empresas que o form consome vem da mesma chamada — getCompaniesRpc(profile), em paralelo via Promise.allSettled.

Editar

Clique no card → UpdateSurveyorForm. Submit valida com Zod e chama updateSurveyorAction(id, data, isSelfUpdate?). A action re-valida com Zod e delega para updateSurveyor em services/surveyors.service.ts, que invoca a edge:

services/surveyors.service.ts typescript
const { data, error } = await supabase.functions.invoke(
  `surveyors-crud/${id}`,
  {
    method: 'PUT',
    body: { userId: user.id, ...body },
  },
);

A edge resolve papel via get_user_by_id_surveyors (mesma RPC SECURITY INVOKER que companies-crud usa) e despacha para update_surveyor_profile, que faz um UPDATE ... SET nome = COALESCE($, nome), ... em mydb.surveyors. Se o payload trouxer email e a linha tiver supabase_auth_id, a edge cria um segundo cliente com SERVICE_ROLE_KEY e chama supabaseAdmin.auth.admin.updateUserById(supabase_auth_id, { email }) para sincronizar o email em auth.users.

Quando o usuário está editando a si mesmo e troca o email, a action marca shouldLogout = true e dispara logout() em seguida — força nova autenticação com o novo email.

app/(main)/surveyors/actions.ts typescript
if (
  isSelfUpdate &&
  currentProfile &&
  validatedData.email &&
  validatedData.email !== currentProfile.email
) {
  shouldLogout = true;
}

await updateSurveyor({ id: surveyorId, ...validatedData });
revalidatePath('/surveyors');

if (shouldLogout) {
  await logout();
}
Atenção

Se a RPC update_surveyor_profile retornar sucesso e auth.admin.updateUserById falhar logo depois, a edge devolve 500 mas não faz rollback. O resultado é uma divergência observável: mydb.surveyors.email com o novo valor e auth.users.email com o antigo. Reconciliação só manualmente pelo painel Supabase ou repetindo a edição até o segundo passo passar.

Excluir

Lixeira do card → ConfirmDialog → se confirmado, deleteSurveyorAction(id)deleteSurveyor em services/surveyors.service.ts:

services/surveyors.service.ts typescript
const { data, error } = await supabase.functions.invoke(
  `surveyors-crud/${surveyorId}`,
  {
    method: 'DELETE',
    body: { userId: user.id },
  },
);
return data.sucesso === true;

A edge resolve papel e empresa do alvo, bloqueia SURVEYOR de deletar surveyor de outra empresa e bloqueia self-delete para SURVEYOR (surveyorUser.id === id). Em seguida chama delete_surveyor_profile(id)DELETE FROM mydb.surveyors WHERE id = $1 RETURNING *. Se a linha apagada tinha supabase_auth_id, a edge chama supabaseAdmin.auth.admin.deleteUser(authId). A ordem é: profile primeiro, Auth depois.

Capítulo 06

Fluxo de convite e definição de senha

Criar SURVEYOR não é só um INSERT em mydb.surveyors. É um fluxo de três passos com email transacional e rollback: o Supabase Auth cria o usuário, a edge grava o perfil, e o convidado recebe email com link para /confirmar-convite, onde define a senha. ADMIN é quem dispara — SURVEYOR e VIEWER não têm botão.

Frontend

ADMIN clica em “Adicionar novo administrador de pesquisa” → CreateSurveyorForm coleta nome, organização, email e telefone. Antes de submeter, abre um ConfirmDialog avisando “Ao adicionar este Administrador de Pesquisa, será disparado um email para {email}. Verifique se o email está correto antes de continuar.”

A Server Action createSurveyorAction revalida o papel (chama fetchUserProfileFromEdge() e exige role === 'ADMIN'), re-roda o Zod, monta o payload (active: 1 fixo) e invoca createSurveyor:

services/surveyors.service.ts typescript
const { data, error } = await supabase.functions.invoke(
  'create-user-surveyor',
  {
    method: 'POST',
    body: payload, // { name, email, phone, company_id, active: 1 }
  },
);

Edge create-user-surveyor (v28, sem código no repo)

O caminho da edge depende do estado do email em auth.users. O cliente da edge é único, com SERVICE_ROLE_KEY e sessão desabilitada. A leitura de usuários é feita via auth.admin.listUsers({ perPage: 1000 }) — sem busca por email, varre até a milésima posição.

  1. Validação do payload. name, email (com regex próprio) e company_id são obrigatórios. phone e active não são validados.
  2. Normalização. email.trim().toLowerCase().
  3. Caso A — usuário não existe. A edge chama:
    supabase/functions/create-user-surveyor (deploy v28) typescript
    await supabaseAdmin.auth.admin.inviteUserByEmail(
      normalizedEmail,
      {
        redirectTo: `${SITE_URL}/confirmar-convite`,
        data: { name: name.trim(), phone: phone?.trim(), company_id },
      },
    );
    O Supabase Auth cria a linha em auth.users e envia o email. Em seguida a edge chama create_full_user_insert_surveyor com p_password = 'remover-essa-coluna', p_active = active ?? 1 e o supabase_auth_id recém-criado. Se a RPC falhar, a edge faz rollback chamando supabaseAdmin.auth.admin.deleteUser(newAuthUserId) e retorna 500.
  4. Caso B — usuário existe e já confirmou email. Retorna 409: “Este usuário já está ativo e não pode ser convidado novamente.”
  5. Caso C — usuário existe e ainda não confirmou. A edge chama auth.admin.generateLink({ type: 'invite', email, options: { redirectTo } }) e responde “O usuário já estava pendente. Um novo convite foi reenviado.”

Definição de senha

O email do Supabase chega com link para SITE_URL/confirmar-convite#access_token=...&refresh_token=.... A rota app/(auth)/confirmar-convite/page.tsx é Client Component: lê tokens do hash, chama supabase.auth.setSession(...), limpa o hash com window.history.replaceState e mostra o form de senha (mínimo 6 caracteres + confirmação) que dispara auth.updateUser({ password }). Documentação completa em auth.html Cap. 04.

Atenção

No Caso C, a edge chama auth.admin.generateLink mas ignora o retorno e responde como se o convite tivesse sido reenviado. O generateLink apenas gera o link; se o email chega ou não depende exclusivamente da configuração do Supabase Auth no Dashboard do projeto (Site URL, Email Provider, template do convite). A mensagem ao ADMIN pode ser otimista enquanto o convidado não recebe nada. Verificar caso a caso no painel ou re-emitir manualmente via inviteUserByEmail.

Capítulo 07

Edge surveyors-crud × RPCs diretas

Surveyors é um dos módulos no meio da migração descrita em backend.html Cap. 05 e em docs/rpc-approach.md. Diferente de Organizações, onde a UI do módulo terminou inteiramente em RPC, aqui o caminho é híbrido: listagem por RPC, mutações por edge. As edges sobrevivem porque precisam de auth.admin (criação de usuário no Auth, sincronização de email, exclusão de usuário) — a diretriz docs/rpc-approach.md marca esses casos explicitamente como KEEP.

Operação Caminho efetivo Caller real
Listar RPC get_surveyors_paginated (ADMIN) ou get_surveyors_by_company (SURVEYOR) app/(main)/surveyors/page.tsx via getSurveyorsRpc. Único caller real da listagem.
Criar Edge create-user-surveyorinviteUserByEmail + RPC create_full_user_insert_surveyor services/surveyors.service.ts:createSurveyor, chamado pela Server Action createSurveyorAction.
Editar Edge surveyors-crud/{id} PUT → RPC update_surveyor_profile + (opcional) auth.admin.updateUserById services/surveyors.service.ts:updateSurveyor, chamado por updateSurveyorAction.
Excluir Edge surveyors-crud/{id} DELETE → RPC delete_surveyor_profile + auth.admin.deleteUser services/surveyors.service.ts:deleteSurveyor, chamado por deleteSurveyorAction.
Listar via edge Edge surveyors-crud POST (sem id) → RPC get_surveyors_paginated/get_surveyors_by_company Sem caller no projeto. A função getSurveyors em services/surveyors.service.ts existe mas nenhum import a usa — vestígio anterior à migração.
GET por id (edge) Edge surveyors-crud GET ?id=N → RPC get_surveyor_by_id Sem caller no projeto. Caminho dormente da edge.

Desde o endurecimento de 2026-06-10, a edge surveyors-crud roda em verify_jwt: true e deriva a identidade do JWT via _shared/auth.ts (getAuthedUser + resolveRole com a variante get_user_by_id_surveyors) — o userId do payload deixou de ser confiável. A autorização é deny-by-default: ADMIN, ou SURVEYOR restrito à própria empresa. Em produção, a listagem nem passa por ela — mas o PUT e o DELETE sim. Catálogo das edges em backend.html Cap. 03.

Nota

A edge create-user-surveyor está deployada na versão 28 mas não existe em supabase/functions/. No lugar, o repo carrega supabase/functions/create-full-user/ — antecessora sem caller real. Para ler o código em produção, usar mcp__supabase__get_edge_function com o slug create-user-surveyor. Aviso geral já em auth.html Cap. 08.

Capítulo 08

Permissões, RLS e RPCs abertas

A matriz é compacta: só ADMIN cria, SURVEYOR edita/exclui dentro da própria empresa, VIEWER nem chega à rota. O gate primário é o Server Component em page.tsx (redireciona VIEWER e carrega o cookie); a edge re-valida em cada mutação.

Papel Acesso à rota /surveyors O que enxerga e pode fazer
ADMIN Sim Vê todos os surveyors via get_surveyors_paginated. Cria, edita e exclui qualquer um. Botão “Adicionar novo administrador de pesquisa” e lixeira por card visíveis. Select de empresa do form habilitado.
SURVEYOR Sim Vê apenas surveyors da própria empresa via get_surveyors_by_company(profile.company_id). Edita qualquer um da empresa (inclusive a si mesmo); exclui qualquer outro da empresa, mas não a si mesmo. Sem botão de adicionar. Select de empresa disabled no form.
VIEWER Não — redirect('/surveys')
Anônimo Bloqueado pelo middleware (sem sessão → /login).

O bloqueio de self-delete tem duas camadas: o card só renderiza a lixeira se role === 'ADMIN' || (role === 'SURVEYOR' && email !== currentUserEmail); a edge surveyors-crud também rejeita com 403 quando o surveyorUser.id === id. Matriz completa do produto em auth.html Cap. 06.

RLS da tabela

mydb.surveyors tem relrowsecurity = true. Duas policies vigoram (consulta MCP em 2026-05-22):

  • Authenticated users can view surveyors — SELECT para {public} com (select auth.role()) = 'authenticated'. Qualquer login lê a tabela inteira via PostgREST com schema('mydb'). Já catalogado na lista “RLS habilitada não escopada” do backend.html Cap. 02.
  • Service role can manage surveyors — ALL para service_role (edges, lambdas).

RPCs com EXECUTE para anon/authenticated

Atenção

Todas as RPCs relevantes do módulo são SECURITY DEFINER (owner postgres) e têm EXECUTE concedido a anon e authenticated (consulta MCP em 2026-05-22). Nenhuma re-valida papel internamente. As mais críticas: public.delete_surveyor_profile(p_id), public.update_surveyor_profile(...), public.create_full_user_insert_surveyor(...), public.get_surveyors_paginated(...) e mydb.get_surveyors_by_company(...). Qualquer cliente com a chave pública anon chama POST /rest/v1/rpc/delete_surveyor_profile com { "p_id": N } e remove o registro de perfil — o auth.users não é apagado nesse caminho (a deleção do Auth só acontece pela edge), mas a linha do mydb some e o usuário fica em estado fantasma. Vetor análogo ao documentado em companies.html Cap. 08. Mitigar com REVOKE EXECUTE ... FROM anon, authenticated ou inserir checagem de papel no corpo da função.

Capítulo 09

Cascata de exclusão e relação com auth.users

A consulta a information_schema.referential_constraints com ccu.table = 'surveyors' devolveu lista vazia (consulta MCP em 2026-05-22): nenhuma foreign key declarada aponta para mydb.surveyors. A única FK que envolve a tabela é a saída fk_supabase_auth_id, que liga supabase_auth_id → auth.users(id) com ON UPDATE CASCADE ON DELETE SET NULL.

O que excluir um surveyor faz

  1. A edge chama delete_surveyor_profile(p_id): DELETE FROM mydb.surveyors WHERE id = $1 RETURNING *.
  2. Se a linha tinha supabase_auth_id, a edge cria um cliente SERVICE_ROLE e chama auth.admin.deleteUser(supabase_auth_id).

Nada mais. Pesquisas criadas pelo SURVEYOR permanecem em mydb.surveys — a tabela de pesquisas é vinculada a company_id, não ao surveyor que criou. Tokens de magic link, respondentes, respostas — todos sobrevivem normalmente. Não há gate “surveyor tem pesquisas em andamento”.

Atenção

A ordem é: profile primeiro, Auth depois. Se auth.admin.deleteUser falhar após o DELETE FROM mydb.surveyors já ter passado, a edge retorna 500 com log “AVISO CRÍTICO” mas não recria a linha. O resultado é uma linha órfã em auth.users que continua válida para login — sem perfil em mydb, o get-profile falha em encontrá-la e o verify-user redireciona para /login?error=unauthorized. O usuário não consegue mais entrar, mas a linha de Auth persiste até remoção manual no Dashboard.

Caminho inverso: se alguém deletar diretamente de auth.users (via Dashboard ou SQL), a FK fk_supabase_auth_id faz SET NULL na coluna correspondente de mydb.surveyors. A linha sobrevive como “fantasma” — o card mostra o ícone “NÃO está sincronizado” (Cap. 03) e o tooltip pede recadastro manual pelo painel Supabase.

Capítulo 10

Integração com outros módulos

  • Autenticação & Papéis: o papel SURVEYOR é derivado pela edge get-profile a partir de match em mydb.surveyors.supabase_auth_id (segundo passo da busca, depois de mydb.users). A rota /confirmar-convite que recebe o convidado é deste módulo de Auth — ver auth.html Cap. 04.
  • Organizações: o company_id do surveyor define o escopo de tudo o que ele enxerga depois (pesquisas, dashboards, grafo, relatórios). O dropdown de empresas no form de criação é populado por getCompaniesRpc(profile) — mesma RPC usada na listagem de organizações. Ausência de FK declarada company_id → companies.id é o mesmo padrão de companies.html Cap. 07.
  • Pesquisas: SURVEYOR só cria, edita e exclui pesquisas da própria empresa; a edge delete-survey usa company_id = profile.company_id para autorizar. Ver surveys.html Cap. 04.
  • Backend (Supabase): a edge surveyors-crud e create-user-surveyor entram no catálogo em backend.html Cap. 03 — ambas em verify_jwt: true desde o endurecimento de 2026-06-10 (antes false). As RPCs do módulo seguem o padrão descrito em backend.html Cap. 04.
Capítulo 11

Pegadinhas

  1. Edge × RPC parcial. Listagem migrou para RPC (rpc/surveyors/get-surveyors.ts); criação, edição e exclusão continuam na edge porque dependem de auth.admin. A função getSurveyors em services/surveyors.service.ts existe mas não tem caller — vestígio anterior à migração. Mexer na edge afeta CRUD; mexer nas RPCs afeta listagem. Não é “a migração terminou”.
  2. RPCs abertas a anon/authenticated. As principais (delete_surveyor_profile, update_surveyor_profile, create_full_user_insert_surveyor, get_surveyors_paginated, get_surveyors_by_company) aceitam chamada direta via PostgREST com a chave pública. Nenhuma re-valida papel. A UI esconder os botões não é gate — só camuflagem. Análogo a companies.html Cap. 08.
  3. Sem FK declarada company_id → companies.id. O índice idx_30703_surveyors_company_id_foreign está nomeado como se fosse índice de FK Laravel, mas a constraint nunca foi criada no Postgres. Excluir uma organização não cascateia (já documentado em companies.html Cap. 07) e trocar o company_id de um surveyor para valor inexistente passa sem erro do banco.
  4. Coluna password legada. Cinco das seis linhas em produção têm o literal 'remover-essa-coluna'; uma carrega o hash bcrypt remanescente da migração Laravel. Autenticação é exclusivamente via auth.users. Já descrito em auth.html Cap. 08.
  5. Update de email pode dessincronizar. Se update_surveyor_profile atualiza mydb.surveyors.email e logo depois auth.admin.updateUserById falha, a edge retorna 500 mas não faz rollback. mydb e auth.users ficam com emails diferentes até alguém conciliar manualmente.
  6. Delete sem cascade nem gate. Excluir um surveyor não apaga pesquisas, tokens nem respostas — não há FK entrando em mydb.surveyors, nem checagem aplicativa “tem pesquisas ativas”. O usuário do auth.users sim é removido. Se o deleteUser falhar depois do DELETE FROM surveyors, sobra Auth órfão (Cap. 09).
  7. Reinvite possivelmente silencioso. No caso C da edge create-user-surveyor (usuário existe e ainda não confirmou), a edge chama auth.admin.generateLink mas não envia email explicitamente. Se o email chega depende exclusivamente da configuração do Supabase Auth no Dashboard. A mensagem “Um novo convite foi reenviado” pode ser otimista.
  8. Edge dormente mas viva. A surveyors-crud tem rotas GET e POST que o frontend não chama (POST de listagem foi substituído pela RPC; GET por id nunca foi usado pela UI). O código continua presente e funcional — alterações na edge precisam considerar as rotas dormentes.
  9. “Pesquisador” × “Administrador de Pesquisa”. Toasts em app/(main)/surveyors/actions.ts usam “Erro ao criar pesquisador.” e “Apenas administradores podem criar pesquisadores.”, enquanto a UI fala em “Administrador de Pesquisa”. Resíduo do nome interno surveyor. Aparecem em momentos distintos, então não chega a confundir, mas vale uniformizar em algum sweep futuro.
  10. Sort coloca o usuário atual no topo. SurveyorsClient reordena o array para que o card do próprio usuário fique na primeira posição. Comportamento de UX, sem efeito de dado — mas pode confundir quem espera ordem determinística da RPC.