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¶
- 02-arquitectura-cliente-servidor.md - Arquitectura dual
- 03-estrategia-datos.md - Clasificación de datos
- ADR-003-offline-first.md - Estrategia offline
- INV-001: Cifrado E2E
ADR actualizado - Enfoque agnóstico de proveedor "PostgreSQL estándar. Sin vendor lock-in. El servidor es ciego."