Cambiar tema

Diseño de bases de datos en Supabase: tablas, relaciones y Row Level Security

Easton editorial illustration: database service control desk

Miras el aviso rojo en el Dashboard de Supabase: «RLS not enabled». Te vienen a la cabeza un montón de preguntas: ¿se filtrarán los datos de los artículos de los usuarios? ¿Está bien esta relación de clave foránea? ¿Cómo se modela de verdad una relación muchos a muchos?

Cuando empecé con Supabase, caí en demasiadas trampas. Creaba tablas y olvidaba activar RLS, así que cualquiera podía leer todo. Diseñaba mal las claves foráneas: borraba un usuario y los artículos seguían ahí. En relaciones muchos a muchos, hasta probé guardar IDs en arrays: un desastre total.

Después de varios meses de prueba y error, por fin entendí el patrón del diseño de bases de datos en Supabase. Este artículo recoge esa experiencia.

1. Estructura de tablas: convenciones de nombres en PostgreSQL

1.1 Convención de nombres: snake_case es lo correcto

PostgreSQL tiene un comportamiento peculiar: si no usas comillas dobles, convierte los identificadores a minúsculas. Si las usas, respeta exactamente lo que escribiste.

¿Qué implica? Con camelCase (UserProfile) tendrías que poner comillas en cada referencia. Demasiado trabajo.

Por eso la convención de la comunidad PostgreSQL es: snake_case (guiones bajos), nombres de tabla en plural y nombres de columna en singular.

-- ✅ Recomendado
CREATE TABLE users (
  id UUID PRIMARY KEY,
  email TEXT UNIQUE,
  created_at TIMESTAMPTZ
);

1.2 Tipos de columna: no te quedes atrapado en la mentalidad MySQL

Error 1: usar VARCHAR en lugar de TEXT

En PostgreSQL, TEXT y VARCHAR tienen el mismo rendimiento. La diferencia es que VARCHAR(n) impone un límite de longitud. Si no necesitas limitarlo, usa TEXT directamente.

Error 2: usar TIMESTAMP en lugar de TIMESTAMPTZ

TIMESTAMP no guarda información de zona horaria. Si el servidor está en EE. UU. y el usuario en China, las horas se mostrarán mal. TIMESTAMPTZ convierte la zona horaria automáticamente.

Error 3: usar SERIAL en lugar de UUID

SERIAL es un entero autoincremental. En una app monolítica puede valer, pero en sistemas distribuidos choca. UUID es único a escala global.

2. Tres tipos de relaciones: uno a uno, uno a muchos, muchos a muchos

2.1 Uno a uno: basta con UNIQUE

El caso más habitual: usuario y perfil.

CREATE TABLE profiles (
  id UUID PRIMARY KEY,
  user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
  bio TEXT
);

Punto clave: user_id UUID UNIQUE. La restricción UNIQUE garantiza un solo perfil por usuario.

2.2 Uno a muchos: la clave foránea clásica

Autores y libros. Un autor puede escribir muchos libros.

CREATE TABLE books (
  id UUID PRIMARY KEY,
  author_id UUID REFERENCES authors(id) ON DELETE CASCADE,
  title TEXT
);

Al consultar, el cliente JS de Supabase puede anidar datos relacionados directamente.

2.3 Muchos a muchos: la tabla puente es clave

Estudiantes y cursos. Un estudiante puede inscribirse en varios cursos y un curso puede tener varios estudiantes.

Solución: crear una tabla de unión.

CREATE TABLE enrollments (
  student_id UUID REFERENCES students(id) ON DELETE CASCADE,
  course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
  PRIMARY KEY (student_id, course_id)
);

3. Row Level Security: la base de datos como guardia de seguridad

3.1 La filosofía de «denegar por defecto» de RLS

La primera vez que usé Supabase, creé una tabla posts y consulté desde el frontend con la anon key. Resultado: devolvió todos los datos. Me asusté.

Resulta que Supabase no activa Row Level Security (RLS) por defecto. Sin RLS, quien tenga la anon key puede leer y escribir todo.

Primera regla de oro: activa RLS en cuanto crees la tabla.

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

¿Y después de activarlo? No basta. RLS activado sin políticas equivale a «denegar todo acceso». Necesitas al menos una política.

3.2 Sintaxis de políticas: USING y WITH CHECK

  • USING: filtra filas existentes (SELECT, UPDATE, DELETE)
  • WITH CHECK: valida filas nuevas (INSERT, UPDATE)

3.3 Cuatro patrones habituales de políticas

Patrón 1: el usuario accede solo a sus datos

CREATE POLICY "Users manage own data"
ON posts FOR ALL
TO authenticated
USING (user_id = auth.uid());

Patrón 2: mezcla de datos públicos y privados

Lo publicado lo ve todo el mundo; los borradores solo el autor.

Patrón 3: aislamiento multi-tenant

Los miembros del equipo solo acceden a datos de su propio equipo.

Patrón 4: control de roles RBAC

Los administradores tienen permisos especiales.

4. Optimización del rendimiento de RLS

4.1 El asesino del rendimiento: subconsultas por fila

Las subconsultas dentro de una política RLS se ejecutan una vez por cada fila. Con 100 000 filas y una subconsulta que comprueba pertenencia a un equipo, la consulta puede tardar 3 minutos.

4.2 Optimización 1: añadir índices

Las columnas usadas en políticas RLS deben tener índice.

CREATE INDEX idx_posts_user_id ON posts(user_id);

Pruebas oficiales de Supabase: sin índice 450 ms, con índice 45 ms. Diez veces más rápido.

4.3 Optimización 2: funciones SECURITY DEFINER

Encapsula la subconsulta en una función que se ejecuta una sola vez.

CREATE OR REPLACE FUNCTION user_teams()
RETURNS SETOF UUID
LANGUAGE SQL SECURITY DEFINER STABLE
AS $$ SELECT team_id FROM team_members WHERE user_id = auth.uid(); $$;

5. Casos prácticos

5.1 Sistema de blog: artículos, categorías y etiquetas

Implementación completa con estructura de tablas, políticas RLS e índices.

5.2 SaaS multi-tenant: colaboración en equipo

Aislamiento de datos por equipo, acceso de miembros y permisos de administrador.

Resumen

Puntos clave:

  • Nomenclatura snake_case
  • Clave primaria UUID, texto TEXT, tiempo TIMESTAMPTZ
  • RLS siempre activado
  • Índices + funciones SECURITY DEFINER para optimizar

FAQ

¿Hay que activar RLS inmediatamente después de crear una tabla?
Sí, es una regla de seguridad básica. Supabase no activa RLS por defecto; quien tenga la anon key puede leer y escribir todos los datos.
¿Por qué PostgreSQL recomienda snake_case?
PostgreSQL convierte los identificadores sin comillas a minúsculas. Con camelCase tendrías que poner comillas dobles en cada uso; es muy incómodo.
¿Por qué no conviene usar FOR ALL en las políticas RLS?
FOR ALL rinde peor que cuatro políticas separadas. Al separarlas, PostgreSQL puede optimizar el uso de índices según cada operación.
¿Cómo comprobar el estado de RLS?
En el Dashboard de Supabase, en la sección de base de datos, revisa el estado RLS de cada tabla. En rojo significa que no está activado.

5 min de lectura · Publicado el: 4 abr 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog