Blog
Architecture
6 de octubre de 20258 min

3 esquemas PostgreSQL para estructurar Supabase correctamente

Puntos clave: Separar una base de datos Supabase PostgreSQL en tres esquemas distintos (privat, collaborative, gamification) desde el principio evita el caos a medida que el proyecto crece. Este artículo cubre el porqué, el cómo, las políticas RLS, las convenciones de nomenclatura y los errores que no debes repetir.

Cuando inicias un proyecto, la tentación es enorme: creas tus tablas en el esquema public de PostgreSQL y avanzas. Una tabla aquí, otra allá. En tres semanas, te encuentras con 40 tablas mezcladas sin ninguna lógica, nombres inconsistentes y un miedo terrible a tocar cualquier cosa.

Decidí muy temprano en el desarrollo de TAMSIV estructurar la base de datos con tres esquemas separados. Seis meses y más de 650 commits después, es probablemente la mejor decisión arquitectónica que he tomado. Aquí te explico por qué y cómo hacerlo concretamente con Supabase.

Sistema de clasificación organizado con carpetas de colores en una oficina moderna
Tres esquemas, como tres armarios de almacenamiento: cada uno tiene su dominio.

¿Por qué no poner todo en el esquema público?

El esquema public de PostgreSQL es el predeterminado. Supabase lo utiliza para sus propias tablas internas. Cuando añades tus tablas allí, mezclas tus datos de negocio con la infraestructura. Es como guardar tu ropa en la cocina — funciona, pero es un caos.

Los problemas concretos que he identificado:

  • Legibilidad: Con más de 40 tablas en un solo esquema, encontrar una tabla se convierte en un juego de adivinanzas.
  • Seguridad: Las políticas RLS (Row Level Security) se vuelven imposibles de razonar cuando coexisten tablas de diferentes dominios.
  • Evolución: Añadir un nuevo dominio funcional (gamificación, analíticas) sin afectar lo existente es imposible si todo está en el mismo saco.
  • Colaboración: Cuando otro desarrollador (o tú mismo en 6 meses) descubre el proyecto, no sabe por dónde empezar.

¿Cómo estructurar los esquemas de una aplicación Supabase?

Aquí están los tres esquemas de TAMSIV y lo que contienen:

Esquema privat — los datos personales

Todo lo que pertenece a un usuario y solo a él:

  • privat.tasks — Tareas personales
  • privat.memos — Notas de voz y texto
  • privat.calendar_events — Eventos del calendario
  • privat.user_profiles — Perfiles de usuario
  • privat.task_attachments / privat.memo_attachments — Archivos adjuntos

¿Por qué privat y no private? Porque private es una palabra reservada en SQL. Lo aprendí por las malas después de una migración fallida que rompió todo el esquema. El mensaje de error ni siquiera era claro — tardé 45 minutos en entender que el nombre del esquema era el problema.

Esquema collaborative — los grupos

Todo lo relacionado con el trabajo en equipo:

  • collaborative.groups — Grupos jerárquicos (hasta 6 niveles)
  • collaborative.group_members — Miembros y roles
  • collaborative.group_tasks / collaborative.group_memos — Contenido compartido
  • collaborative.checklists — Listas de verificación con validación

Este esquema es el más complejo en términos de políticas RLS porque las reglas de acceso dependen del rol del usuario en el grupo, la jerarquía de los grupos y los permisos heredados. He detallado la complejidad de los grupos jerárquicos en el artículo dedicado.

Esquema gamification — el compromiso

Todo lo que hace que la aplicación sea adictiva (en el buen sentido):

  • gamification.user_stats — Puntos, nivel, racha actual
  • gamification.user_badges — Insignias desbloqueables
  • gamification.points_history — Historial detallado de puntos
  • gamification.daily_challenges — Desafíos diarios
  • gamification.feed_activity — Flujo de actividad social

Este esquema se añadió tres meses después del inicio del proyecto. Gracias a la separación, creé un nuevo esquema sin tocar los otros dos. Cero riesgo de romper lo existente. La arquitectura de gamificación se detalla en el artículo sobre el esquema de gamificación.

Pizarra con diagrama de esquema de base de datos dibujado con marcadores azules y verdes
La planificación del esquema en la pizarra antes de tocar una sola línea de SQL.

¿Qué ventajas concretas en la práctica?

Más allá de la teoría, esto es lo que la separación en esquemas me ha aportado en el día a día:

Legibilidad inmediata

Cuando hago SELECT * FROM privat.tasks, sé instantáneamente que es un dato personal. Cuando veo collaborative.group_members, sé que está relacionado con los grupos. No necesito documentación adicional — el esquema ES la documentación.

Políticas RLS más fáciles de razonar

Las reglas del esquema privat son claras: solo ves tus datos. Punto. La política RLS se resume en una línea:

CREATE POLICY "users_own_data" ON privat.tasks
  USING (user_id = auth.uid());

Las del esquema collaborative son más complejas: ves los datos de los grupos de los que eres miembro, según tu rol, con herencia de permisos parentales. Pero como están aisladas en su esquema, la complejidad no se desborda al resto.

Evolución sin riesgo

Añadir la gamificación no requirió ninguna modificación de los esquemas existentes. Tampoco añadir la agenda con participantes y filtros. Cada nuevo dominio funcional es autónomo.

¿Cómo gestionar las políticas RLS con múltiples esquemas?

Supabase utiliza Row Level Security (RLS) para asegurar el acceso a los datos. El principio es simple: cada tabla tiene reglas que determinan quién puede leer, escribir, modificar, eliminar.

En la práctica, es un laberinto. En total, TAMSIV cuenta con más de 30 políticas RLS, cada una probada individualmente. Así es como las organizo:

  • Esquema privat: Políticas simples, basadas en auth.uid() = user_id.
  • Esquema collaborative: Políticas complejas que verifican la pertenencia al grupo Y el rol. Uso de subconsultas en collaborative.group_members.
  • Esquema gamification: Políticas mixtas — lectura pública para la clasificación, escritura solo a través de funciones RPC del servidor.

La trampa más común: una política RLS demasiado permisiva que permite la fuga de datos entre usuarios. Hablo de ello en el artículo sobre la auditoría de seguridad y el rate limiting.

¿Qué convenciones de nomenclatura adoptar?

Convenciones estrictas desde el día 1 evitan horas de confusión más tarde. Aquí están las de TAMSIV:

  • Tablas: snake_case en plural (tasks, group_members, daily_challenges)
  • Columnas: snake_case (user_id, created_at, is_completed)
  • Parámetros RPC: Prefijo p_ (p_start_date, p_group_id, p_user_id)
  • Funciones RPC: verbo_objeto (get_consolidated_feed, add_gamification_points)

El prefijo p_ para los parámetros RPC es crítico. Sin él, el día que tu parámetro user_id entre en conflicto con la columna user_id en una consulta, PostgreSQL no lanzará un error — tomará la columna como prioridad. El resultado: una consulta que devuelve datos inesperados, un error silencioso y peligroso.

¿Cómo migrar a múltiples esquemas si el proyecto ya existe?

Si ya tienes todo en public y quieres migrar, aquí tienes la estrategia:

  1. Auditoría: Lista todas tus tablas y clasifícalas por dominio funcional.
  2. Crea los esquemas: CREATE SCHEMA privat; CREATE SCHEMA collaborative;
  3. Migra tabla por tabla: ALTER TABLE public.tasks SET SCHEMA privat;
  4. Actualiza las políticas RLS: Están vinculadas a la tabla, por lo que siguen el movimiento.
  5. Actualiza el código frontend: Todas las consultas de Supabase ahora deben especificar el esquema.

Atención: las claves foráneas, los triggers y las funciones RPC deben actualizarse manualmente. Es un gran trabajo, pero la ganancia en mantenibilidad es enorme. Si usas Supabase, lee también mi artículo sobre la reducción del egreso para optimizar los costos.

Candado de seguridad sobre un teclado de portátil con iluminación azul
La seguridad de los datos comienza con una buena arquitectura de base.

¿Qué errores evitar al estructurar?

En 6 meses de desarrollo en solitario, he identificado varias trampas:

  • No usar palabras reservadas de SQL como nombres de esquema. private, public, user están todos reservados. Usa alternativas (privat, app_public, accounts).
  • No olvidar los permisos GRANT. Crear un esquema no es suficiente — debes dar explícitamente los derechos de acceso a los roles de Supabase (anon, authenticated, service_role).
  • Nunca confíes en los archivos de migración locales. La base de datos real es la única fuente de verdad. Los archivos de migración pueden divergir después de correcciones manuales.
  • Probar cada política RLS individualmente. Más de 30 políticas significan al menos más de 30 escenarios de prueba. Es metódico, aburrido y absolutamente indispensable.

¿Qué impacto tiene en el rendimiento?

Buenas noticias: los esquemas de PostgreSQL no tienen ningún impacto en el rendimiento de las consultas. Un SELECT FROM privat.tasks es exactamente tan rápido como un SELECT FROM public.tasks. Los esquemas son una organización lógica, no física.

Sin embargo, la separación facilita la optimización. Puedes añadir índices específicos por dominio, configurar parámetros de vacuum diferentes por esquema y monitorear el rendimiento por dominio funcional.

Para el frontend, el refactoring clean architecture también ha jugado un papel crucial en la mantenibilidad del código que interactúa con la base de datos.

Preguntas Frecuentes

¿Cuántos esquemas hay que crear para una aplicación clásica?

Dos a cuatro son suficientes para la mayoría de los proyectos. Uno para los datos del usuario, uno para las funcionalidades compartidas/colaborativas, eventualmente uno para analíticas o gamificación. Lo importante es separar por dominio funcional, no por tipo de dato.

¿Supabase soporta bien los esquemas múltiples?

Sí, pero hay que configurar los permisos manualmente. Por defecto, solo el esquema public es accesible a través de la API REST. Para exponer otro esquema, hay que añadirlo en los parámetros del proyecto Supabase y configurar los GRANT apropiados en los roles anon y authenticated.

¿Se necesitan claves foráneas entre esquemas?

Sí, y está perfectamente soportado por PostgreSQL. Una tabla en collaborative puede referenciar una tabla en privat. Las restricciones de clave foránea funcionan entre esquemas sin ninguna limitación.

¿Cómo gestionar las funciones RPC que afectan a varios esquemas?

Las funciones RPC (como get_consolidated_feed) pueden unir tablas de diferentes esquemas sin problema. La buena práctica es crear la función en el esquema del dominio principal que sirve, y usar nombres completamente calificados (privat.tasks) en las consultas.

¿Se puede volver atrás después de migrar a varios esquemas?

Técnicamente sí, con ALTER TABLE SET SCHEMA public. Pero en la práctica, una vez que el código frontend referencia los esquemas, el retroceso es costoso. Por eso es mejor tomar esta decisión temprano en el proyecto.