Cómo implementar roles y permisos en Supabase paso a paso
Supabase se apoya en PostgreSQL y su Row Level Security (RLS) para controlar el acceso a cada fila de tus tablas. En esta guía aprenderás, paso a paso, a diseñar roles, crear tablas de membresía, activar RLS, escribir policies seguras y cubrir casos comunes (CRUD por propietario, organizaciones, Storage y “admin global”).
1) Conceptos clave en 1 minuto
- Roles de API:
anon(usuarios no autenticados),authenticated(con sesión) yservice_role(clave del servidor con permisos totales; nunca en el cliente). - RLS: al activarlo, todo queda denegado por defecto. Concedes acceso mediante políticas (
USINGpara leer/actualizar/borrar yWITH CHECKpara crear/actualizar). - Helpers JWT: en SQL puedes usar
auth.uid()(UUID del usuario),auth.email()yauth.jwt()(claims JSON) para leer metadatos del token.
2) Modelo mínimo de datos (organizaciones y membresías)
Definimos una tabla de organizaciones y otra de membresías con un rol por miembro.
-- 2.1) Tipo enum para roles internos
create type public.org_role as enum ('owner','admin','member','viewer');
-- 2.2) Organizaciones
create table if not exists public.organizations (
id uuid primary key default gen_random_uuid(),
name text not null,
created_at timestamptz not null default now()
);
-- 2.3) Membresías (usuario -> organización)
create table if not exists public.org_members (
org_id uuid references public.organizations(id) on delete cascade,
user_id uuid not null, -- coincide con auth.users.id
role public.org_role not null default 'member',
created_at timestamptz not null default now(),
primary key (org_id, user_id)
);
-- 2.4) Recurso ejemplo: proyectos bajo una organización
create table if not exists public.projects (
id uuid primary key default gen_random_uuid(),
org_id uuid not null references public.organizations(id) on delete cascade,
title text not null,
owner_id uuid not null, -- dueño (normalmente creador)
created_at timestamptz not null default now()
);
3) Activa RLS y crea funciones de ayuda
alter table public.organizations enable row level security;
alter table public.org_members enable row level security;
alter table public.projects enable row level security;
-- Helper: ¿el usuario actual tiene alguno de estos roles en la org?
create or replace function public.is_org_role(p_org uuid, roles text[])
returns boolean language sql stable as $$
select exists(
select 1
from public.org_members m
where m.org_id = p_org
and m.user_id = auth.uid()
and m.role::text = any(roles)
);
$$;
-- Helper: ¿el usuario actual es miembro de la org?
create or replace function public.is_org_member(p_org uuid)
returns boolean language sql stable as $$
select public.is_org_role(p_org, array['owner','admin','member','viewer']);
$$;
4) Políticas base por tabla
4.1) organizations
Solo miembros pueden verla; crear/editar solo “owner”/“admin”.
-- SELECT: visible solo para miembros
create policy orgs_select on public.organizations
for select
using ( public.is_org_member(id) );
-- INSERT: crear org si estás autenticado (te harás owner en un trigger o en la app)
create policy orgs_insert on public.organizations
for insert
with check ( auth.uid() is not null );
-- UPDATE/DELETE: solo roles elevados
create policy orgs_update on public.organizations
for update
using ( public.is_org_role(id, array['owner','admin']) );
create policy orgs_delete on public.organizations
for delete
using ( public.is_org_role(id, array['owner']) );
4.2) org_members
Lectura para miembros; gestionar miembros solo “owner”/“admin”.
-- SELECT: un miembro puede ver el listado de su org
create policy members_select on public.org_members
for select
using ( public.is_org_member(org_id) );
-- INSERT: añadir miembros si eres owner/admin
create policy members_insert on public.org_members
for insert
with check ( public.is_org_role(org_id, array['owner','admin']) );
-- UPDATE (cambiar rol) y DELETE (expulsar): owner/admin
create policy members_update on public.org_members
for update
using ( public.is_org_role(org_id, array['owner','admin']) );
create policy members_delete on public.org_members
for delete
using ( public.is_org_role(org_id, array['owner','admin']) );
4.3) projects
Lectura para miembros; crear proyectos si eres miembro; actualizar/eliminar si eres dueño o admin.
-- SELECT: cualquiera de la organización
create policy projects_select on public.projects
for select
using ( public.is_org_member(org_id) );
-- INSERT: cualquier miembro puede crear; asegúrate de setear owner_id = auth.uid()
create policy projects_insert on public.projects
for insert
with check (
public.is_org_member(org_id)
and owner_id = auth.uid()
);
-- UPDATE/DELETE: dueño o admin
create policy projects_update on public.projects
for update
using (
owner_id = auth.uid() or public.is_org_role(org_id, array['owner','admin'])
);
create policy projects_delete on public.projects
for delete
using (
owner_id = auth.uid() or public.is_org_role(org_id, array['owner','admin'])
);
5) Patrón “admin global” con claims personalizados
Si necesitas un superadmin que salte entre organizaciones, añade una claim en el JWT (p. ej. app_metadata.role = "superadmin") desde tu backend usando la clave service_role. Luego léela en las policies.
-- Política que permite todo si el JWT trae app_metadata.role = 'superadmin'
create policy orgs_superadmin_all on public.organizations
for all
using (
coalesce( (auth.jwt() -> 'app_metadata' ->> 'role'), '' ) = 'superadmin'
)
with check (
coalesce( (auth.jwt() -> 'app_metadata' ->> 'role'), '' ) = 'superadmin'
);
Nota: no intentes usar la claim role para “convertirte” en un rol de Postgres. Los roles de DB (anon/authenticated) los fija Supabase. Usa auth.jwt() para leer claims personalizadas dentro de las políticas.
6) Storage (archivos) con políticas
Los buckets de Storage también usan RLS. Ejemplo: bucket avatars, cada usuario solo ve y escribe lo suyo; admins de la organización pueden leer todo.
-- Crear bucket (Hazlo en el dashboard o via RPC)
-- Políticas:
create policy storage_read_own on storage.objects
for select
using (
bucket_id = 'avatars'
and (owner = auth.uid()) -- asumiendo que guardas owner (uuid) en metadata o path
);
create policy storage_upload_own on storage.objects
for insert
with check (
bucket_id = 'avatars'
and (owner = auth.uid())
);
-- Lectura por admin de la org (si almacenas org_id en metadata)
create policy storage_read_org_admin on storage.objects
for select
using (
bucket_id = 'avatars'
and public.is_org_role( (metadata ->> 'org_id')::uuid, array['owner','admin'] )
);
7) Triggers útiles
Cuando crees una organización, suele convenir asignar automáticamente al creador como owner.
create or replace function public.add_owner_after_org_insert()
returns trigger language plpgsql as $$
begin
insert into public.org_members(org_id, user_id, role)
values (new.id, auth.uid(), 'owner');
return new;
end; $$;
drop trigger if exists t_org_owner on public.organizations;
create trigger t_org_owner
after insert on public.organizations
for each row execute procedure public.add_owner_after_org_insert();
8) Uso desde React / React Native
En el cliente, no necesitas lógica extra para permisos: las políticas RLS protegen el acceso. Solo envía el JWT del usuario.
// Ejemplo con supabase-js
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Seleccionar proyectos visibles para el usuario actual
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('org_id', selectedOrgId);
// Insert (owner_id se fija al usuario actual)
const { error: errInsert } = await supabase
.from('projects')
.insert({ org_id: selectedOrgId, title: 'Nuevo', owner_id: (await supabase.auth.getUser()).data.user.id });
9) Pruebas y depuración
- Usa el Policy Debugger del dashboard: ejecuta acciones como “anon”, “authenticated” o con un usuario concreto.
- Recuerda: con RLS activo, cualquier operación sin policy aplicable será denegada.
- Evita políticas demasiado permisivas como
using (true)salvo casos públicos muy justificados.
10) Errores comunes (y cómo evitarlos)
- Olvidar WITH CHECK: permite que cualquiera inserte filas que luego no podrá ver. Especifica siempre la condición para insert/update.
- Confiar lógica sensible al cliente: las decisiones de permiso deben vivir en las policies, no en el frontend.
- No guardar referencias: para políticas por organización, guarda siempre
org_id(y opcionalmenteowner_id). - Usar service_role en el cliente: jamás. Resérvalo para backends/Edge Functions.
Conclusión
Con RLS y policies bien diseñadas, Supabase te permite implementar roles y permisos robustos sin servidores intermedios. Parte de un modelo claro (membresías y roles), activa RLS desde el día 1 y construye políticas pequeñas, específicas y testeables. Así obtendrás seguridad por defecto y un backend listo para escalar.
#Supabase #RLS #Permisos #Roles #PostgreSQL #Seguridad #BackendAsAService #React #ReactNative #JavaScript