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.
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. |
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. Defaultnextval('mydb.surveyors_id_seq'). É osurveyor_idusado 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 aplicaformatPhoneapenas para exibição; o que é gravado é a string formatada do input.varchar, NOT NULL. Coberto por UNIQUE indexidx_30703_surveyors_email_unique.- password
varchar, NOT NULL. Coluna legada que não autentica. A edgecreate-user-surveyorgrava o literal'remover-essa-coluna'; a senha real vive emauth.userse é atualizada porsupabase.auth.updateUser({ password })na rota/confirmar-convite.- active
smallint, NOT NULL, default1. A criação grava sempre1. 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 índiceidx_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, defaultCURRENT_TIMESTAMP.updated_até atualizado pela RPCupdate_surveyor_profilecomNOW(); sem trigger.- supabase_auth_id
uuid, nullable. FK declaradafk_supabase_auth_idparaauth.users(id)comON UPDATE CASCADE ON DELETE SET NULL. É a ponte que oget-profileusa para derivar o papel (ver auth.html Cap. 02).
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.
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. |
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.
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.
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.
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.
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:
- ADMIN →
supabase.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_iddefinido →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_idou 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:
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.
if (
isSelfUpdate &&
currentProfile &&
validatedData.email &&
validatedData.email !== currentProfile.email
) {
shouldLogout = true;
}
await updateSurveyor({ id: surveyorId, ...validatedData });
revalidatePath('/surveyors');
if (shouldLogout) {
await logout();
}
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:
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.
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:
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.
-
Validação do payload.
name,email(com regex próprio) ecompany_idsão obrigatórios.phoneeactivenão são validados. -
Normalização.
email.trim().toLowerCase(). -
Caso A — usuário não existe. A edge chama:
O Supabase Auth cria a linha emsupabase/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 }, }, );auth.userse envia o email. Em seguida a edge chamacreate_full_user_insert_surveyorcomp_password = 'remover-essa-coluna',p_active = active ?? 1e osupabase_auth_idrecém-criado. Se a RPC falhar, a edge faz rollback chamandosupabaseAdmin.auth.admin.deleteUser(newAuthUserId)e retorna 500. - Caso B — usuário existe e já confirmou email. Retorna 409: “Este usuário já está ativo e não pode ser convidado novamente.”
-
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.
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.
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-surveyor → inviteUserByEmail + 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.
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.
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 comschema('mydb'). Já catalogado na lista “RLS habilitada não escopada” do backend.html Cap. 02. -
Service role can manage surveyors— ALL paraservice_role(edges, lambdas).
RPCs com EXECUTE para anon/authenticated
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.
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
-
A edge chama
delete_surveyor_profile(p_id):DELETE FROM mydb.surveyors WHERE id = $1 RETURNING *. -
Se a linha tinha
supabase_auth_id, a edge cria um clienteSERVICE_ROLEe chamaauth.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”.
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.
Integração com outros módulos
-
Autenticação & Papéis:
o papel SURVEYOR é derivado pela edge
get-profilea partir de match emmydb.surveyors.supabase_auth_id(segundo passo da busca, depois demydb.users). A rota/confirmar-conviteque recebe o convidado é deste módulo de Auth — ver auth.html Cap. 04. -
Organizações: o
company_iddo 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 porgetCompaniesRpc(profile)— mesma RPC usada na listagem de organizações. Ausência de FK declaradacompany_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-surveyusacompany_id = profile.company_idpara autorizar. Ver surveys.html Cap. 04. -
Backend (Supabase): a
edge
surveyors-crudecreate-user-surveyorentram no catálogo em backend.html Cap. 03 — ambas emverify_jwt: truedesde o endurecimento de 2026-06-10 (antesfalse). As RPCs do módulo seguem o padrão descrito em backend.html Cap. 04.
Pegadinhas
-
Edge × RPC parcial. Listagem migrou para RPC
(
rpc/surveyors/get-surveyors.ts); criação, edição e exclusão continuam na edge porque dependem deauth.admin. A funçãogetSurveyorsemservices/surveyors.service.tsexiste 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”. -
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. -
Sem FK declarada
company_id → companies.id. O índiceidx_30703_surveyors_company_id_foreignestá 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 ocompany_idde um surveyor para valor inexistente passa sem erro do banco. -
Coluna
passwordlegada. 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 viaauth.users. Já descrito em auth.html Cap. 08. -
Update de email pode dessincronizar. Se
update_surveyor_profileatualizamydb.surveyors.emaile logo depoisauth.admin.updateUserByIdfalha, a edge retorna 500 mas não faz rollback.mydbeauth.usersficam com emails diferentes até alguém conciliar manualmente. -
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 doauth.userssim é removido. Se odeleteUserfalhar depois doDELETE FROM surveyors, sobra Auth órfão (Cap. 09). -
Reinvite possivelmente silencioso. No caso C da
edge
create-user-surveyor(usuário existe e ainda não confirmou), a edge chamaauth.admin.generateLinkmas 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. -
Edge dormente mas viva. A
surveyors-crudtem 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. -
“Pesquisador” × “Administrador de
Pesquisa”. Toasts em
app/(main)/surveyors/actions.tsusam “Erro ao criar pesquisador.” e “Apenas administradores podem criar pesquisadores.”, enquanto a UI fala em “Administrador de Pesquisa”. Resíduo do nome internosurveyor. Aparecem em momentos distintos, então não chega a confundir, mas vale uniformizar em algum sweep futuro. -
Sort coloca o usuário atual no topo.
SurveyorsClientreordena 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.