Saltar al contenido principal

Estándares de Bases de Datos

Estándares de calidad para Postgres en Supabase (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 DELETE explí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_at se actualiza automáticamente vía trigger.

Soft delete (obligatorio)​

  • Soft delete: set deleted_at = now() (no usar DELETE).
  • 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 DELETE explícito:
    • RESTRICT para relaciones críticas,
    • CASCADE solo si el hijo no tiene sentido sin padre,
    • SET NULL si el FK es opcional y el huérfano es válido.
  • CHECK constraints para valores acotados (montos > 0, status, etc.).
  • TEXT preferido sobre VARCHAR(n) salvo que el límite sea regla de negocio.

Indexing baseline​

  • Indexar FKs usadas en JOIN/WHERE.
  • Indexar columnas de ORDER BY en 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, int a uuid)
    • 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.

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_id enviado 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_case plural y columnas en snake_case singular.
  • Tiene columnas requeridas: id, created_at, updated_at, deleted_at.
  • updated_at se 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 DELETE explí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 NULL donde 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).