---
title: "Supabase raw_user_meta_data: cómo un registro fue Owner"
description: "Un trigger handle_new_user tomaba role_id de raw_user_meta_data, que fija el navegador. Cómo cualquiera fue Owner, la corrección con invitaciones, una prueba."
url: "https://madsalt.tech/es/notes/supabase-raw-user-meta-data-role/"
author: "Denys Zaichenko"
language: "es"
published: 2026-10-18
---

# Supabase raw_user_meta_data: cómo un registro fue Owner

En resumen

`raw_user_meta_data` contiene lo que el navegador pasó a `supabase.auth.signUp()`. En una aplicación en producción, el trigger `handle_new_user` leía de ahí el rol del nuevo usuario, así que cualquiera podía registrarse con el id del rol Owner y quedarse con la cuenta. La corrección: el rol sale de una tabla que solo escribe el servidor, rellenada antes de enviar la invitación.

Fue en el [control de horas de una firma de auditoría](https://madsalt.tech/es/work/audit-firm-time-tracking/), una aplicación Supabase donde cada regla de acceso vive en la base de datos. El bug estaba en el trigger que crea un perfil para cada usuario nuevo.

## ¿Qué hacía el trigger?

Simplificado, era así:

```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;
$$;
```

El flujo de invitación ponía ahí el `role_id` correcto, así que en el uso normal funcionaba. Pero `raw_user_meta_data` son las `options.data` de un registro, y es el navegador quien decide qué lleva:

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

La comprobación de permisos devolvía true para los owners antes de mirar nada más, así que ese único campo daba control total de la cuenta. La propia [guía de Users](https://supabase.com/docs/guides/auth/users) de Supabase dice que los metadatos del usuario no deben usarse en políticas RLS ni en la lógica de autorización, porque el usuario puede editarlos sin ninguna comprobación.

![De dónde salía el rol de un usuario nuevo. El trigger handle_new_user antes y después de la corrección. Antes: cualquiera que se registre: signUp options.data → raw_user_meta_data → handle_new_user → Rol Owner; Después: solo los invitados: Función de invitación → pending_invites → handle_new_user → Rol de invitado.](https://madsalt.tech/charts/es/supabase-raw-user-meta-data-role.svg)

*Del trigger de registro de una aplicación Supabase en producción, junio de 2026.*

## ¿Qué lo arregló?

Ahora el rol sale de una tabla privada a la que el cliente no puede 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;
```

La función de invitación se ejecuta en el servidor, con el permiso de quien invita ya comprobado. Primero escribe la invitación pendiente y solo después crea el enlace de invitación. El trigger busca el rol por el correo y borra la fila. El nombre sigue viniendo de los metadatos, y no pasa nada: son datos para mostrar.

El orden importa. Una invitación crea el usuario al instante: la [guía de Users](https://supabase.com/docs/guides/auth/users) de Supabase señala que invitar a un correo nuevo crea un usuario sin confirmar. El trigger se dispara en ese momento, así que la fila pendiente ya debe existir.

## ¿Cómo comprobar su propia aplicación?

Abra su función `handle_new_user`, o cualquier trigger sobre `auth.users`, y busque `raw_user_meta_data`. El nombre y el avatar de ahí están bien. El rol, la empresa, el plan, el inquilino o cualquier otra cosa que dé acceso debe salir de una tabla o de `raw_app_meta_data`, que el usuario no puede cambiar.

Después pruébelo: regístrese por la API con `role_id` o `role: 'admin'` en `options.data`. La cuenta nueva no debería recibir nada extra.

Este tipo de comprobación es el primer paso de mi [método para probar RLS](https://madsalt.tech/es/blog/how-to-test-supabase-rls/). En aplicaciones multi-tenant, vea también [lo que RLS no cubre para un usuario en dos orgs](https://madsalt.tech/es/notes/supabase-rls-user-in-two-organizations/). Los otros controles que ejecuto en cada app de Supabase están en mi [lista de seguridad para apps Lovable](https://madsalt.tech/es/blog/lovable-app-security-checklist/). La primera que miro en el Dashboard es [qué esquemas expone la Data API](https://madsalt.tech/es/notes/supabase-exposed-schemas/). La [Auditoría de seguridad de Supabase](https://madsalt.tech/es/tools/supabase-security-check/) cubre roles, grants, políticas y ajustes de auth con 46 reglas de OWASP Top 10:2025, no lo que un trigger copia de los metadatos del registro, así que esta sigue siendo una comprobación manual.
