Estándares de Bases de Datos
Estándares de calidad para Postgres en Supabase o Lovable Cloud (schema, migraciones, RLS y ciclo de vida de datos).
Estos estándares son independientes a la IA: aplican igual si el cambio lo hace una persona o una herramienta.
Glosario
- RLS (Row-Level Security): Seguridad a nivel de fila en Postgres. Permite definir políticas para que cada usuario solo pueda leer/escribir las filas que le corresponden (por ejemplo,
user_id = auth.uid()), incluso si intenta hacer queries directas fuera del frontend. - PII (Personally Identifiable Information): Información Personal Identificable. Datos que pueden identificar a una persona directa o indirectamente (ej. nombre, email, teléfono, documento, dirección). Requiere mayor cuidado: mínimo acceso, retención definida y protección con RLS cuando aplique.
- PK (Primary Key): Clave primaria. Columna (o conjunto de columnas) que identifica de forma única cada fila de una tabla (por ejemplo,
id). - FK (Foreign Key): Clave foránea. Columna que referencia la PK (u otra columna única) de otra tabla para mantener integridad referencial (por ejemplo,
orders.user_id → users.id). - Tabla user-owned (por usuario): Tabla donde cada registro “pertenece” a un usuario específico (normalmente porque tiene un
user_id). La regla general es que cada usuario solo puede ver/editar sus propios registros. - Service role: Tipo de usuario con permisos elevados (administrador) en Supabase. Puede ejecutar operaciones saltándose RLS. Se usa solo para tareas internas/admin y siempre con autorización explícita.
Alcance
Incluye:
- Diseño de tablas, naming conventions, integridad de datos e índices.
- Migraciones y checklist de release.
- RLS (Row-Level Security) y baseline de acceso.
- Soft delete, retención, PII (Personally Identifiable Information) y hard delete.
Reglas no negociables
- Todos los cambios de schema van por migraciones (nunca editar schema en Dashboard en prod/stg).
- Cada tabla nueva debe tener:
id,created_at,updated_at,deleted_at+ RLS habilitado. - Delete por defecto = soft delete (
deleted_at = now()). Hard delete solo para casos MUY concretos. - Todas las timestamps son
timestamptz(UTC). - Toda FK debe tener
ON DELETEexplícito. - RLS en tablas con datos de usuario/sensibles (por defecto en nuevas tablas); omitir RLS solo si es lookup público y documentado.
- Migraciones append-only: nunca editar o borrar migraciones ya creadas.
Naming conventions
- Tablas:
snake_case, plural (ej.order_items). - Columnas:
snake_case, singular (ej.user_id). - FKs:
<tabla_referenciada_singular>_id(ej.user_id,order_id). - Índices:
idx_<table>_<columns>. - Unique constraints:
uq_<table>_<columns>. - Check constraints:
chk_<table>_<desc>. - Boolean: prefijo
is_/has_. - Policies RLS:
<table>_<operation>_<actor>(ej.orders_select_owner).
Columnas requeridas (todas las tablas)
id uuid PRIMARY KEY DEFAULT gen_random_uuid()created_at timestamptz NOT NULL DEFAULT now()updated_at timestamptz NOT NULL DEFAULT now()deleted_at timestamptz(soft delete)updated_atse actualiza automáticamente vía trigger.
Soft delete (obligatorio)
- Soft delete: set
deleted_at = now()(no usarDELETE). - Default queries: siempre filtrar
deleted_at IS NULL. - Restore: set
deleted_at = NULL+ logging del evento. - Hard delete solo para casos muy concretos que deben quedar documentados.
Integridad de datos
- FKs con
ON DELETEexplícito:RESTRICTpara relaciones críticas,CASCADEsolo si el hijo no tiene sentido sin padre,SET NULLsi el FK es opcional y el huérfano es válido.
CHECKconstraints para valores acotados (montos > 0, status, etc.).TEXTpreferido sobreVARCHAR(n)salvo que el límite sea regla de negocio.
Indexing baseline
- Indexar FKs usadas en JOIN/WHERE.
- Indexar columnas de
ORDER BYen tablas grandes. - Soft delete: preferir partial index
WHERE deleted_at IS NULL. - No indexar “por si acaso”: solo con patrón de query claro.
- Índices compuestos: orden por mayor selectividad primero.
Migraciones y releases
Reglas
- Una migración = un cambio lógico (tabla/policies/índices).
- RLS + policies para tabla nueva van en la misma migración.
- Cambios incompatibles se desacoplan (no romper código y DB en el mismo paso).
Naming
- Usa este formato:
<timestamp>_<descripcion_en_snake_case>.sql
Cambios incompatibles (patrones obligatorios)
Cuando un cambio podría romper el código existente o afectar datos en producción, no se hace en un solo paso. Se hace por etapas (normalmente en migraciones separadas):
- Renombrar una columna
- Crear la nueva columna → copiar los datos a la nueva (backfill) → actualizar el código para usarla → eliminar la columna vieja.
- Cambiar el tipo de dato de una columna (por ejemplo,
intauuid)- Crear una nueva columna con el tipo correcto → copiar/convertir los datos (backfill) → cambiar el código → eliminar o reemplazar la columna vieja.
- Hacer una columna obligatoria (
NOT NULL)- Primero asegurar que todas las filas tengan un valor:
- Poner un valor por defecto o
- Rellenar los valores existentes (backfill)
- Luego aplicar
NOT NULL.
- Primero asegurar que todas las filas tengan un valor:
RLS & baseline de acceso
- RLS requerido en:
- Tablas que tengan user_id o que referencien auth.users.
- Tablas que almacenen PII o cualquier dato sensible.
- Por defecto, toda tabla nueva debe crearse asumiendo que requiere RLS. Solo se puede omitir en casos específicos (por ejemplo, tablas lookup públicas).
Reglas mínimas recomendadas (tablas “por usuario”)
Cuando una tabla tiene datos que pertenecen a un usuario (por ejemplo, tiene user_id), estas son las reglas base:
- Ver y editar (SELECT/UPDATE): cada usuario solo puede ver o modificar sus propios registros, y solo los que no estén eliminados (soft delete).
- Crear (INSERT): al crear un registro, el sistema debe guardar automáticamente que pertenece al usuario autenticado (no aceptar un
user_idenviado por el cliente). - Eliminar (DELETE): en tablas con soft delete, los usuarios no deben borrar registros físicamente. En su lugar, se marca como eliminado (por ejemplo, con
deleted_at) y eso se maneja desde la app/edge.
Service role (límites)
- La service role tiene permisos “de administrador” y puede saltarse las reglas normales de acceso (RLS).
Por eso solo se usa para operaciones internas del sistema o acciones de administración, y siempre con:- Validación de permisos (autorización) antes de ejecutar
- No se debe usar service role para endpoints normales de usuarios si RLS ya cubre el acceso. Si lo necesitas para “que funcione”, probablemente falta una policy o falta autorización.
Ciclo de vida, retención y PII
- No loggear PII (email, phone, address, etc.); nunca retornar PII en errores.
- PII debe estar en tablas con RLS + policies owner-only.
- No guardar PII en JSONB mezclado: aislar en columnas dedicadas.
Checklist de calidad (DB)
A) Nueva tabla
- Tabla en
snake_caseplural y columnas ensnake_casesingular. - Tiene columnas requeridas:
id,created_at,updated_at,deleted_at. -
updated_atse mantiene con trigger (incluido en la migración). - RLS habilitado en la misma migración.
- Policies mínimas (SELECT + INSERT al menos) definidas y nombradas correctamente.
- Todas las FKs tienen
ON DELETEexplícito. - Índices para FKs y patrones claros de WHERE/ORDER BY (incluye soft delete index si aplica).
B) Archivo de migración
- Nombre correcto:
<timestamp>_<descripcion>.sql(UTC,snake_case). - Probado local:
supabase db reset+supabase migration up. - Sin valores environment-specific (UUIDs hardcoded, etc.).
- Cambios incompatibles están separados por fases (add/backfill/swap/drop).
- No se editó/reescribió una migración existente (append-only).
C) RLS & acceso
- RLS habilitado (Con excepción de casos concretos).
- Policies siguen baseline: owner-only +
deleted_at IS NULLdonde aplique. - No existe policy de DELETE para usuarios en tablas con soft delete.
- Si se requiere service role: está justificado (y el consumo se hará desde Edge Functions con auth).
D) Soft delete / hard delete
- La operación de delete es soft delete (
deleted_at = now()). - Queries user-facing filtran
deleted_at IS NULL. - Si hay hard delete, debe ser para casos muy concretos que deben quedar documentados.
E) Release (DB)
- Migración alineada con cambios de app/edge (orden: migración primero → código después).
- Índices justificados (patrones documentados).
- No se expone información sensible en logs/errores (PII/tokens).