Saltar a contenido

ADR-004: Selección de Base de Datos (Agnóstico de Proveedor)

Estado: ACEPTADO Fecha: 2025-12-07 Actualizado: 2025-12-07 (Enfoque agnóstico de proveedor) Autor: DatabaseDrone (MTS-DRN-DBM-001) Revisores: SpecQueen, Director


1. Contexto

MedTime necesita:

  • Base de datos relacional para datos estructurados
  • Almacenamiento de blobs cifrados E2E (servidor Zero-Knowledge)
  • SQL estándar sin dependencia de features específicos de proveedores
  • Escalabilidad y costo predecible
  • Capacidad de migrar entre proveedores si es necesario

2. Decisión

Adoptamos PostgreSQLcomo base de datos principal, con un enfoqueagnóstico de proveedor:

  • NO depender de features específicos de Supabase, AWS RDS, GCP Cloud SQL, etc.
  • NO usar Row Level Security (RLS) como mecanismo principal de seguridad
  • Implementar autorización en la capa de aplicación
  • Usar SQL estándar portable entre proveedores

2.1. Stack de Datos

Capa Tecnología Propósito
Backend DB PostgreSQL (cualquier proveedor) Datos principales (blobs E2E)
iOS Local Core Data Almacenamiento offline
Android Local Room Almacenamiento offline
Cache Redis (opcional) Sesiones, rate limiting

2.2. Principios de Diseño

PRINCIPIOS AGNÓSTICOS:
+------------------------------------------------------------------+
| 1. SQL ESTÁNDAR                                                    |
|    - Solo features disponibles en PostgreSQL vanilla               |
|    - Sin extensiones propietarias del proveedor                    |
|    - Migraciones portables                                         |
+------------------------------------------------------------------+
| 2. AUTORIZACIÓN EN APLICACIÓN                                      |
|    - NO depender de RLS para seguridad                             |
|    - Validar user_id en cada query desde la aplicación             |
|    - Tokens JWT validados en capa de API                           |
+------------------------------------------------------------------+
| 3. ZERO-KNOWLEDGE                                                  |
|    - Servidor almacena blobs opacos (cifrado E2E)                  |
|    - Sin lógica de negocio sobre contenido de blobs                |
|    - Solo metadata operativa en claro                              |
+------------------------------------------------------------------+

2.3. Esquema Típico (SQL Estándar)

-- Ejemplo de tabla para datos cifrados E2E
-- Portable a cualquier PostgreSQL (Supabase, AWS RDS, GCP, self-hosted)

CREATE TABLE encrypted_user_data (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    entity_type VARCHAR(50) NOT NULL,

    -- Datos cifrados E2E (servidor NO puede leer)
    encrypted_blob BYTEA NOT NULL,
    blob_hash VARCHAR(64) NOT NULL,
    blob_size_bytes INT NOT NULL,
    encryption_version VARCHAR(10) NOT NULL DEFAULT '1.0',

    -- Metadata operativa (servidor SÍ puede leer)
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    sync_version BIGINT NOT NULL DEFAULT 1,

    -- Índices ciegos para búsqueda
    blind_index_1 VARCHAR(64),
    blind_index_2 VARCHAR(64)
);

-- Índices estándar
CREATE INDEX idx_user_entity ON encrypted_user_data(user_id, entity_type);
CREATE INDEX idx_sync_version ON encrypted_user_data(user_id, sync_version);
CREATE INDEX idx_blind_1 ON encrypted_user_data(blind_index_1)
    WHERE blind_index_1 IS NOT NULL;

-- NOTA: Autorización se implementa en la capa de aplicación
-- Cada query DEBE incluir WHERE user_id = :authenticated_user_id

2.4. Autorización en Aplicación (NO en RLS)

# PSEUDOCÓDIGO - Autorización en capa de aplicación

async def get_user_data(user_id_from_token: UUID, entity_type: str):
    """
    La autorización se valida AQUÍ, no en RLS.
    """
    # El user_id viene del JWT validado
    query = """
        SELECT * FROM encrypted_user_data
        WHERE user_id = :user_id AND entity_type = :entity_type
    """

    # user_id siempre viene del token, NUNCA del request body
    return await db.execute(query, {
        "user_id": user_id_from_token,
        "entity_type": entity_type
    })

3. Consecuencias

3.1. Positivas

  • Portabilidad: Fácil migración entre proveedores (Supabase → AWS → GCP → self-hosted)
  • Sin vendor lock-in: No dependemos de features propietarios
  • SQL estándar: Desarrolladores conocen el stack
  • Seguridad en aplicación: Control total sobre autorización
  • Zero-Knowledge: Servidor inherentemente no puede leer datos

3.2. Negativas

  • Más código: Autorización manual en cada endpoint
  • Sin real-time nativo: Si se necesita, implementar con WebSockets propios
  • Responsabilidad: Equipo debe asegurar que TODAS las queries filtran por user_id

3.3. Riesgos

  • Query sin user_id: Un bug podría exponer datos de otros usuarios
  • Mitigación: Code review obligatorio, tests automatizados, principio de mínimo privilegio en credenciales de DB

4. Alternativas Consideradas

4.1. Supabase con RLS

  • Rechazado porque: Crea dependencia del proveedor, RLS es específico de Supabase/PostgreSQL

4.2. Firebase Firestore

  • Rechazado porque: NoSQL, difícil para datos relacionales, vendor lock-in extremo

4.3. MongoDB

  • Rechazado porque: NoSQL, menos adecuado para queries relacionales

4.4. CockroachDB

  • Rechazado porque: Over-engineering para nuestro caso, aunque es una buena opción futura

5. Proveedores Compatibles

Esta arquitectura funciona con cualquiera de estos proveedores:

Proveedor Tipo Notas
Supabase Managed ✅ Compatible (sin usar RLS)
AWS RDS Managed ✅ Compatible
GCP Cloud SQL Managed ✅ Compatible
Azure PostgreSQL Managed ✅ Compatible
DigitalOcean Managed ✅ Compatible
Self-hosted On-premise ✅ Compatible
Neon Serverless ✅ Compatible
Railway PaaS ✅ Compatible

6. Referencias


ADR actualizado - Enfoque agnóstico de proveedor "PostgreSQL estándar. Sin vendor lock-in. El servidor es ciego."