---
title: "Supabase raw_user_meta_data: como um registo virou Owner"
description: "O trigger handle_new_user lia o role_id de raw_user_meta_data, definido pelo browser no registo. Como qualquer pessoa virava Owner e a correção."
url: "https://madsalt.tech/pt/notes/supabase-raw-user-meta-data-role/"
author: "Denys Zaichenko"
language: "pt"
published: 2026-10-18
---

# Supabase raw_user_meta_data: como um registo virou Owner

Em resumo

`raw_user_meta_data` guarda tudo o que o browser passou a `supabase.auth.signUp()`. Numa aplicação em produção, o trigger `handle_new_user` lia dali a função do novo utilizador, por isso qualquer pessoa podia registar-se com o id da função Owner e ficar dona da conta. A correção: a função vem de uma tabela em que só o servidor escreve, preenchida antes de o convite ser enviado.

Isto foi no [registo de horas da empresa de auditoria](https://madsalt.tech/pt/work/audit-firm-time-tracking/), uma aplicação Supabase em que todas as regras de acesso vivem na base de dados. O bug estava no trigger que cria um perfil para cada novo utilizador.

## O que fazia o trigger?

Simplificado, era assim:

```sql
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
declare
  _role_id uuid := (new.raw_user_meta_data ->> 'role_id')::uuid;
begin
  insert into public.profiles (id, full_name, role_id)
  values (new.id, new.raw_user_meta_data ->> 'full_name', _role_id);
  return new;
end;
$$;
```

O fluxo de convite colocava ali o `role_id` certo, por isso no uso normal funcionava. Mas `raw_user_meta_data` é o `options.data` de um registo, e é o browser que decide o que lá entra:

```js
await supabase.auth.signUp({
  email: 'me@example.com',
  password: '...',
  options: { data: { role_id: '<owner role uuid>' } },
})
```

A verificação de permissões devolvia verdadeiro para os owners antes de olhar para qualquer outra coisa, por isso esse único campo dava controlo total da conta. O próprio [guia de Users](https://supabase.com/docs/guides/auth/users) do Supabase diz que os metadados do utilizador não devem ser usados em políticas de RLS nem em lógica de autorização, porque o utilizador pode editá-los sem qualquer verificação.

![De onde vinha a função de um novo utilizador. O trigger handle_new_user antes e depois da correção. Antes: quem se registasse: signUp options.data → raw_user_meta_data → handle_new_user → Função Owner; Depois: só quem foi convidado: Função de convite → pending_invites → handle_new_user → Função do convite.](https://madsalt.tech/charts/pt/supabase-raw-user-meta-data-role.svg)

*Do trigger de registo de uma aplicação Supabase em produção, junho de 2026.*

## O que resolveu?

A função passou a vir de uma tabela privada em que o cliente não pode tocar:

```sql
create table private.pending_invites (
  email text primary key,
  role_id uuid,
  invited_by uuid,
  created_at timestamptz default now()
);
revoke all on table private.pending_invites from anon, authenticated;
```

A função de convite corre no servidor, com a permissão de quem convida verificada. Escreve primeiro o convite pendente e só depois cria a ligação do convite. O trigger procura a função pelo email e apaga a linha. O nome continua a vir dos metadados, o que não faz mal: são dados de apresentação.

A ordem importa. Um convite cria o utilizador de imediato: o [guia de Users](https://supabase.com/docs/guides/auth/users) do Supabase indica que convidar um email novo cria um utilizador não confirmado. O trigger dispara nesse momento, por isso a linha pendente já tem de existir.

## Como verificar a sua aplicação?

Abra a sua função `handle_new_user`, ou qualquer trigger em `auth.users`, e procure `raw_user_meta_data`. O nome e o avatar vindos dali não têm problema. Função, empresa, plano, tenant ou qualquer outra coisa que dê acesso tem de vir de uma tabela ou de `raw_app_meta_data`, que o utilizador não pode alterar.

Depois experimente: registe-se pela API com `role_id` ou `role: 'admin'` em `options.data`. A nova conta não deve receber nada a mais.

Este tipo de verificação é o primeiro passo do meu [método de testes de RLS](https://madsalt.tech/pt/blog/how-to-test-supabase-rls/). Para aplicações multi-tenant, veja também [o que o RLS não apanha num utilizador em duas orgs](https://madsalt.tech/pt/notes/supabase-rls-user-in-two-organizations/). As outras verificações que faço em todas as apps Supabase estão na minha [lista de segurança para apps Lovable](https://madsalt.tech/pt/blog/lovable-app-security-checklist/). A primeira que vejo no dashboard é [que schemas a Data API expõe](https://madsalt.tech/pt/notes/supabase-exposed-schemas/). A [Auditoria de segurança Supabase](https://madsalt.tech/pt/tools/supabase-security-check/) cobre funções, grants, políticas e definições de auth com 46 regras do OWASP Top 10:2025, não o que um trigger copia dos metadados do registo, por isso esta continua a ser uma verificação manual.
