
El problema del multi-tenant
Una aplicación SaaS suele tener una única base de datos compartida por todos los clientes (tenants). Cada fila de cada tabla pertenece a un tenant_id concreto. El mayor riesgo: una bug en la aplicación que olvide el WHERE tenant_id = ? y devuelva filas de otro cliente.
Soluciones tradicionales:
- Base de datos por tenant: aísla perfectamente pero escala mal a miles de tenants.
- Esquema por tenant: algo menos aislado, aún complejo.
- Filtro en la aplicación: depende de que cada query lo incluya correctamente. Un fallo humano y se filtra información.
- Row-Level Security: el motor aplica el filtro automáticamente. Aunque la aplicación haga
SELECT * FROM pedidossin WHERE, PostgreSQL devuelve solo las filas del tenant actual.
RLS es la opción más robusta y la preferida en SaaS serios (Supabase la usa como fundamento de su capa de seguridad).
Activar RLS en una tabla
Dos pasos: activar RLS y crear al menos una policy.
-- 1. Activar RLS
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
-- Si no hay ninguna policy, RLS bloquea TODAS las filas por defecto.
-- Esto es correcto por seguridad: si te olvidas la policy, no hay fuga.
Importante: los superusers y los dueños de la tabla pueden saltarse RLS por defecto. Para que apliquen también: FORCE ROW LEVEL SECURITY.
Definir policies
Una policy define una condición de visibilidad. La sintaxis:
CREATE POLICY nombre_policy ON tabla
[AS PERMISSIVE | RESTRICTIVE]
[FOR {ALL | SELECT | INSERT | UPDATE | DELETE}]
[TO rol_o_lista]
[USING (expresion_booleana)]
[WITH CHECK (expresion_booleana)];
USING: filtro aplicado a lectura (SELECT) y a la condición de UPDATE/DELETE.WITH CHECK: filtro aplicado a escrituras (INSERT, resultado de UPDATE). Sin WITH CHECK, se usa el USING.PERMISSIVE(default): si hay varias policies, se combinan con OR.RESTRICTIVE: se combinan con AND con el resto. Útil para "además debe cumplir X".
Ejemplo canónico: multi-tenant
-- Tabla
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
cliente TEXT,
total NUMERIC
);
-- Activar RLS
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
-- Policy: cada tenant solo ve sus filas
CREATE POLICY pedidos_por_tenant ON pedidos
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::INT)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::INT);
En la aplicación, al abrir cada transacción, se indica el tenant:
# Python + psycopg2
with conn:
with conn.cursor() as cur:
cur.execute("SET LOCAL app.tenant_id = %s", (tenant_actual,))
# Todas las queries de esta transaccion ven solo filas de ese tenant
cur.execute("SELECT * FROM pedidos") # filtra automaticamente
SET LOCAL aplica solo a la transacción actual; al hacer COMMIT/ROLLBACK se borra. Así cada request de HTTP puede establecer su propio tenant.
Varios tipos de policy para distintos roles
-- Policy 1: operadores ven todo en su tenant
CREATE POLICY operador_tenant ON pedidos
FOR ALL TO rol_operador
USING (tenant_id = current_setting('app.tenant_id')::INT);
-- Policy 2: clientes solo ven SUS pedidos dentro del tenant
CREATE POLICY cliente_propios ON pedidos
FOR SELECT TO rol_cliente
USING (
tenant_id = current_setting('app.tenant_id')::INT
AND cliente_id = current_setting('app.user_id')::INT
);
-- Policy 3: admin sin restriccion (pero asegurar auditoria aparte)
CREATE POLICY admin_todo ON pedidos
FOR ALL TO rol_admin
USING (true);
Cuando varias policies coinciden (mismo comando, mismo rol), se combinan según PERMISSIVE/RESTRICTIVE.
USING vs WITH CHECK: subtileza
USING se aplica a qué filas son visibles; WITH CHECK a qué filas se pueden crear/modificar. Ejemplo claro: permitir transferir un pedido a otro tenant:
-- USING permite leer solo los propios
-- WITH CHECK permite crear/mover a cualquier tenant
CREATE POLICY pedidos_ver_propio ON pedidos
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::INT);
CREATE POLICY pedidos_crear_cualquiera ON pedidos
FOR INSERT WITH CHECK (tenant_id IS NOT NULL);
Un rol con las dos policies: ve solo sus pedidos, pero puede insertar en cualquier tenant. Raro, pero ilustra la diferencia.
RESTRICTIVE policies
Por defecto, múltiples PERMISSIVE se combinan con OR: si alguna permite, se deja pasar. Si quieres que se apliquen todas con AND, usa RESTRICTIVE:
-- Todos deben cumplir: (A) estar en su tenant Y (B) ser un registro no archivado
CREATE POLICY solo_tenant ON pedidos
AS PERMISSIVE FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::INT);
CREATE POLICY no_archivados ON pedidos
AS RESTRICTIVE FOR ALL
USING (archivado = FALSE);
Una policy RESTRICTIVE actúa como un requisito adicional, no alternativa.
Bypass y superusuarios
Roles con BYPASSRLS
Un rol con el atributo BYPASSRLS ignora todas las policies. Útil para mantenimiento, ETL, backups:
CREATE ROLE etl_job WITH LOGIN BYPASSRLS PASSWORD '...';
Cuidado con usarlo en aplicaciones productivas: pierdes el beneficio de RLS.
FORCE ROW LEVEL SECURITY
Por defecto, el owner de la tabla y los superusers ignoran RLS. Para forzar que también ellos lo respeten:
ALTER TABLE pedidos FORCE ROW LEVEL SECURITY;
Recomendable en SaaS donde ni siquiera los DBAs deben ver datos de otros tenants sin consentimiento.
Inspeccionar policies
-- Ver policies existentes
SELECT schemaname, tablename, policyname, roles, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'pedidos';
-- Ver tablas con RLS activo
SELECT oid::regclass AS tabla, relrowsecurity, relforcerowsecurity
FROM pg_class
WHERE relrowsecurity = TRUE;
Caso real: Supabase Auth
Supabase, que usa PostgreSQL por debajo, apoya toda su API REST en RLS:
-- Habilita RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- La funcion auth.uid() devuelve el ID del usuario autenticado (JWT)
CREATE POLICY "Usuarios ven sus posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Usuarios crean sus posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Usuarios actualizan sus posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
Cualquier request autenticado que haga SELECT * FROM posts automáticamente ve solo los suyos. No hace falta WHERE user_id = ? en la aplicación.
Performance
Las policies son una cláusula WHERE automática. Su coste depende de:
- Selectividad: una policy
WHERE tenant_id = Xcon índice en tenant_id es rápida. - Subqueries en USING: evítalas. Si la policy necesita hacer un JOIN, el coste se multiplica por fila.
Siempre indexa la columna del discriminador:
CREATE INDEX idx_pedidos_tenant ON pedidos(tenant_id);
Verifica con EXPLAIN ANALYZE que la policy usa el índice.
Buenas prácticas
- Piensa en los roles antes que en las policies. Define bien qué actores hay (superadmin, admin, usuario, service_role).
- Un service_role con BYPASSRLS para operaciones del backend (cron, ETL) evita hacks.
- SET LOCAL siempre sobre SET (LOCAL vive solo durante la transacción, SET persiste toda la conexión y es peligroso con pooling).
- Auditoría: registra en un log quién accede a qué. RLS previene fugas, pero no sustituye auditoría.
- Prueba las policies: haz tests que se autentican con roles distintos y verifican que ven/no ven filas esperadas.
- Documenta las policies como código versionado en migraciones, no creadas a mano desde psql.
Limitaciones
- RLS aplica a queries normales, no a COPY FROM (solo owner o BYPASSRLS).
- Las policies pueden ralentizar el planner si son complejas; verifica con EXPLAIN.
pg_dumpcon--no-security-labelsignora RLS y puede leer todo (es owner).- Algunas extensiones (pg_stat_statements) pueden mostrar queries originales; evaluar si es aceptable.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SQL es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de SQL
Explora más contenido relacionado con SQL y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Activar RLS en una tabla con ENABLE ROW LEVEL SECURITY y definir POLICY con USING y WITH CHECK. Crear multiples policies combinadas (PERMISSIVE vs RESTRICTIVE). Pasar contexto de la app a la BD con SET LOCAL y current_setting(). Aplicar RLS a un escenario multi-tenant donde cada cliente solo ve sus filas. Bypassear RLS para roles privilegiados con BYPASSRLS o policies condicionales. Auditar que policies aplican a cada rol con pg_policies.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje