Saltar a contenido

03 - Estrategia de Datos

Versión: 1.1 Última Actualización: 2025-12-08 Estado: Aprobado


1. Clasificación de Datos

MedTime clasifica datos en 6 categorías según su ubicación, sincronización y tratamiento de seguridad:

Clasificación Ubicación Sincronización Cifrado Ejemplo
LOCAL_ONLY Cliente No Aplicable Caché de UI, logs locales, preferencias de usuario, patrones ML
SYNCED_E2E Cliente + Servidor Sí, E2E Siempre Recetas OCR, diagnósticos, registros médicos, medicamentos
SYNCED_HASH Cliente + Servidor Sí, hash verificable Condicionalmente Email, teléfono (Blind Index)
SYNCED_PLAIN Cliente + Servidor Sí, plano No Tiempos de sincronización, versiones de datos, tier
SERVER_SOURCE Servidor Búsqueda Online Requerido Catálogos públicos >100 registros (medicamentos, interacciones)
ANONYMIZED_DATA Cliente → Servidor Sí, anonimizado No (ya anonimizado) Datos para enriquecer catálogos públicos

1.1. Sobre SERVER_SOURCE

REGLA DE 100 REGISTROS (Directiva del Director):

Los catálogos públicos siguen estas reglas estrictas:

Tamaño del Catálogo Estrategia Se Descarga al Cliente Notas
>100 registros Búsqueda ONLINE only ❌ NO Búsqueda por API en tiempo real
≤100 registros Embebido en app ✅ SÍ Puede incluirse en el bundle de la app
Registros seleccionados Local + Sync E2E ✅ SÍ Solo los que el usuario elige/usa

Excepción: Mini-cache de <100 interaccionesCONTRAINDICADO críticas puede almacenarse localmente para validación offline de seguridad.

Ver: 02-arquitectura-cliente-servidor.md sección 3.2.3.1 para detalles completos.

1.1.1. Construcción de Catálogos Locales del Usuario

Los clientes NO cachean catálogos públicos completos. En su lugar, construyen catálogos personales combinando:

  1. Búsquedas online en catálogos >100 registros (resultados no se cachean)
  2. Registros seleccionados por el usuario (guardados como SYNCED_E2E)
  3. Entradas manuales del usuario (guardadas como SYNCED_E2E)

El catálogo personal del usuario (medicamentos elegidos, análisis usados, etc.) se sincroniza como SYNCED_E2E.

1.2. Sobre ANONYMIZED_DATA

Datos que se envían sin vínculo a la identidad del usuario para enriquecer los catálogos públicos de MedTime.

1.2.1. Consentimiento

  • OBLIGATORIO para usuarios con cuenta (no es opt-in)
  • Se obtiene durante el onboarding (explícito, conforme LFPDPPP)
  • Requisito para usar la aplicación con cuenta
  • Ver INV-009: Consentimiento Datos de Salud

1.2.2. Catálogos Enriquecibles

Catálogo Datos de Usuario Especificación
Medicamentos Medicamentos ingresados manualmente INV-010
Estudios/Tratamientos Resultados de laboratorio, tratamientos INV-011
Interacciones Med-Med Combinaciones usadas, efectos observados INV-012
Interacciones Med-Estudio Medicamentos que afectan estudios INV-013
Efectos Secundarios Efectos adversos reportados INV-014

Tabla de catálogos públicos con su clasificación según tamaño:

Catálogo Tamaño Estimado Estrategia Descarga al Cliente Justificación
Medicamentos México ~15,000 registros ❌ Búsqueda Online NO Demasiado grande, búsqueda por API
Interacciones Med-Med ~500,000 pares ❌ Búsqueda Online NO Consulta por API con pares específicos
Interacciones Med-Estudio ~50,000 pares ❌ Búsqueda Online NO Consulta por API bajo demanda
Estudios Clínicos Comunes ~200 registros ❌ Búsqueda Online NO Excede umbral de 100
Efectos Secundarios ~10,000 registros ❌ Búsqueda Online NO Base de datos farmacológica
Contraindicaciones Críticas <100 pares ✅ Mini-cache Local SÍ (excepción) Seguridad offline, validación crítica
Unidades de Medida ~30 registros ✅ Embebido Pequeño, estático
Vías de Administración ~15 registros ✅ Embebido Pequeño, estático
Frecuencias de Dosis ~20 registros ✅ Embebido Pequeño, estático

Notas Importantes:

  • Medicamentos del Usuario: Los medicamentos que el usuario ha seleccionado o agregado manualmente se guardan localmente como SYNCED_E2E, no como parte del catálogo público.
  • Cache de Búsquedas: NO se implementa cache de resultados de búsquedas de catálogos >100 registros.
  • Contraindicaciones Críticas: Única excepción, debe contener <100 interacciones de alto riesgo (ej: warfarina + aspirina, IMAO + SSRIs) para validación offline.

1.3.1. Principio de Anonimización: Separación de Identidad

NO es generalización a categorías. MedTime envía datos específicos pero elimina todos los identificadores:

Lo que SÍ se envía (anonimizado):

  • Nombres específicos (medicamento, estudio, tratamiento)
  • Resultados en rangos (bucketing): "Glucosa 140-150" no "147"
  • Región generalizada: "Centro Mexico" no "CDMX"
  • Período: "2025-Q4" no "2025-12-07"

Lo que NUNCA se envía (eliminado):

  • user_id, email, nombre del usuario
  • device_id, IP
  • Timestamps exactos
  • Valores exactos de resultados
  • Cualquier identificador directo o indirecto

1.3.2. Umbrales de Seguridad

Tipo de Dato k-anonymity Mínimo Tratamiento Especial
Medicamentos comunes >= 1000 Reportar
Estudios comunes >= 1000 Reportar con bucketing
Efectos secundarios leves >= 1000 Generalizar a categoría (GI, Neuro)
Efectos secundarios severos NUNCA Suprimir completamente
Medicamentos sensibles (VIH, cáncer) NUNCA Suprimir completamente

2. Esquemas Conceptuales

2.1. Entidades Principales - Cliente (LocalStorage)

Entidad Clasificación Retención Sincronización
User SYNCED_HASH Indefinida Hash públicamente verificable
MedicationSchedule SYNCED_E2E Indefinida Cifrado E2E
MedicationLog SYNCED_E2E 2 años Cifrado E2E
Appointment SYNCED_HASH 1 año Hash verificable
DiagnosisRecord SYNCED_E2E Indefinida Cifrado E2E
RecipeOCR SYNCED_E2E Indefinida Cifrado E2E
AnomalyLog SYNCED_E2E 1 año Cifrado E2E
UserPreferences LOCAL_ONLY Indefinida No sincroniza
UICache LOCAL_ONLY 24 horas No sincroniza

2.2. Entidades Principales - Servidor (Database)

Entidad Clasificación Indexación Auditable Retención
User SERVER_SOURCE PK: user_id Indefinida
Device SERVER_SOURCE FK: user_id Indefinida
MedicationSchedule SYNCED_E2E FK: user_id Indefinida
MedicationLog SYNCED_E2E FK: user_id, FK: schedule_id 2 años
Appointment SYNCED_HASH FK: user_id 1 año
DiagnosisRecord SYNCED_E2E FK: user_id Indefinida
RecipeOCR SYNCED_E2E FK: user_id Indefinida
AnomalyLog SYNCED_E2E FK: user_id, FK: medication_id 1 año
AccessLog SERVER_SOURCE FK: user_id, FK: device_id 90 días
AuditTrail SERVER_SOURCE FK: resource_id 7 años

2.3. Mapeo Cliente-Servidor

CLIENTE (LocalDB)                SERVIDOR (PostgreSQL)
┌─────────────────────┐          ┌─────────────────────┐
│ User (SYNCED_HASH)  │ ◄────►   │ User (SERVER_SOURCE)│
│ MedicSchedule (E2E) │ ◄────►   │ MedicSchedule (E2E) │
│ MedicLog (E2E)      │ ◄────►   │ MedicLog (E2E)      │
│ Appointment (HASH)  │ ◄────►   │ Appointment (HASH)  │
│ DiagRecord (E2E)    │ ◄────►   │ DiagRecord (E2E)    │
│ RecipeOCR (E2E)     │ ◄────►   │ RecipeOCR (E2E)     │
│ AnomalyLog (E2E)    │ ◄────►   │ AnomalyLog (E2E)    │
│ UserPrefs (ONLY)    │          │ AccessLog (SRC)     │
│ UICache (ONLY)      │          │ AuditTrail (SRC)    │
└─────────────────────┘          └─────────────────────┘

3. Sincronización

3.1. Cola de Operaciones (Operation Queue)

Cada cliente mantiene una cola de operaciones pendientes:

Campo Tipo Descripción
queue_id UUID Identificador único de operación
entity_type String Tipo de entidad (MedicSchedule, MedicLog, etc.)
entity_id UUID ID de la entidad
operation Enum CREATE, UPDATE, DELETE
payload JSON Datos serializado (cifrados si E2E)
timestamp DateTime Timestamp local de creación
status Enum PENDING, SYNCED, FAILED
retry_count Int Número de reintentos (máx 5)

Flujo:

  1. Operación local se agrega a queue con status = PENDING
  2. Sincronización intenta enviar al servidor
  3. Si success: status = SYNCED
  4. Si error: retry_count++, reintenta en 30s, 1m, 5m, 15m, 60m
  5. Si falla después de 5 intentos: status = FAILED, notifica usuario

3.2. Resolución de Conflictos

Clasificación Conflicto Estrategia
SYNCED_E2E Actualizaciones en paralelo Merge manual: usuario elige versión
SYNCED_E2E Eliminación vs. Actualización Merge manual: usuario decide si mantener o eliminar
SYNCED_HASH Conflicto de metadata Server timestamp wins (servidor es fuente de verdad)
SYNCED_HASH Hash mismatch Cliente recibe versión servidor, recalcula hash
SERVER_SOURCE Cualquier conflicto No aplica, cliente siempre recibe estado servidor

Resolución Manual E2E:

  • Usuario ve versión local y versión servidor en paralelo
  • Puede elegir: mantener local, aceptar servidor, o merge manual
  • Ambas versiones permanecen en auditoria

4. Retención y Backup

4.1. Políticas de Retención por Clasificación

Clasificación Cliente Servidor GDPR Right to Erasure
LOCAL_ONLY 24 horas (configurable) No aplica N/A
SYNCED_E2E Indefinida (encriptada) Indefinida Sí, full deletion
SYNCED_HASH 1 año (configurable) 1 año (configurable) Sí, hash removal
SYNCED_PLAIN 90 días 90 días Sí, full deletion
SERVER_SOURCE No aplica 7 años (logs/audits) Sí, con redacción

4.2. Backup Strategy

Cliente (Mobile):

  • Daily incremental backup to encrypted local cache
  • User can export full backup as encrypted file (.medtime_backup)
  • Backup stored in Documents/MedTime/ (user-controlled location)
  • Expiration: None unless user deletes

Servidor:

  • Continuous WAL (Write-Ahead Logging) backup
  • Daily full backup + hourly incremental
  • Replication a standby PostgreSQL
  • Retention: 30 días de backups (PITR capable)
  • Encryption at rest: AES-256

4.3. Recuperación de Desastres

RTO (Recovery Time Objective): 4 horas RPO (Recovery Point Objective): 1 hora

  • Standby disponible para failover automático
  • Client syncroniza desde último checkpoint conocido
  • Auditoria de recuperación registrada en AuditTrail

5. Integridad y Verificación

5.1. Checksums y Hashes

  • SYNCED_E2E: SHA-256 de contenido cifrado (verificable sin decriptar)
  • SYNCED_HASH: SHA-256 de contenido plano (verificable por cliente y servidor)
  • SYNCED_PLAIN: CRC32 para detección de corrupción en tránsito
  • SERVER_SOURCE: Verificación de integridad en base de datos nativa

5.2. Auditable Operations

Todas las operaciones en SYNCED_E2E y SERVER_SOURCE generan entrada de AuditTrail:

  • Quién: user_id + device_id
  • Qué: operation + entity_type + entity_id
  • Cuándo: timestamp server + timezone
  • Dónde: IP + user_agent
  • Por qué: reason (si aplica)
  • Hash: SHA-256 de toda la entrada

6. Estrategia de Particionado para Tablas de Alto Volumen (DV2-P3)

6.1. Contexto: DB-BAJO-004

Problema: A medida que MedTime escale, ciertas tablas del servidor pueden crecer rapidamente (millones de registros), impactando performance de queries, indices y mantenimiento. El particionado es una estrategia para dividir tablas grandes en segmentos manejables.

6.2. Tablas Candidatas para Particionado

6.2.1. Analisis de Volumetria (Proyeccion 1 ano)

Tabla Registros/Usuario/Ano Usuarios Objetivo Total Estimado Crecimiento Candidata
cli_dose_logs ~1,095 (3/dia) 100K ~110M Alto ✅ SI
srv_ocr_requests ~50 (Perfect only) 10K ~500K Medio ✅ SI
srv_prescription_sync ~100 50K ~5M Bajo ⚠️ Considerar
srv_audit_trail ~1,000 100K ~100M Alto ✅ SI
srv_access_log ~3,650 (10/dia) 100K ~365M Muy Alto ✅ SI
cli_medications ~5 100K ~500K Bajo ❌ NO
srv_drug_catalog N/A (catalogo) N/A ~50K Muy Bajo ❌ NO

Conclusiones:

  • ALTO volumen: cli_dose_logs, srv_audit_trail, srv_access_log
  • MEDIO volumen: srv_ocr_requests
  • BAJO volumen: Resto (no requieren particionado en v1.0)

6.3. Estrategia de Particionado por Tabla

6.3.1. cli_dose_logs (Cliente - SYNCED_E2E)

NO aplica particionado en cliente:

  • SQLite/Realm NO soportan particionado nativo
  • Cada cliente tiene volumen moderado (~1K registros/ano)
  • Cleanup automatico de logs > 2 anos mitiga crecimiento

Alternativa: Archivado de logs antiguos a tabla separada cli_dose_logs_archive.

6.3.2. srv_audit_trail (Servidor - Logs de Auditoria)

Estrategia: Particionado porrango de tiempo (RANGE) mensual.

-- Tabla padre
CREATE TABLE srv_audit_trail (
    audit_id        UUID NOT NULL,
    user_id         UUID NOT NULL,
    action          VARCHAR(50) NOT NULL,
    entity_type     VARCHAR(50) NOT NULL,
    entity_id       UUID NOT NULL,
    details         JSONB,
    ip_address      INET,
    user_agent      TEXT,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    PRIMARY KEY (audit_id, created_at)
) PARTITION BY RANGE (created_at);

-- Particiones mensuales (ejemplo para 2025-Q1)
CREATE TABLE srv_audit_trail_2025_01 PARTITION OF srv_audit_trail
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE srv_audit_trail_2025_02 PARTITION OF srv_audit_trail
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

CREATE TABLE srv_audit_trail_2025_03 PARTITION OF srv_audit_trail
    FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');

-- Indices en cada particion (automaticos si se crean en tabla padre)
CREATE INDEX idx_audit_trail_user_created ON srv_audit_trail(user_id, created_at);
CREATE INDEX idx_audit_trail_entity ON srv_audit_trail(entity_type, entity_id);

Ventajas:

  • Queries filtrados por fecha usan solo particiones relevantes (pruning)
  • Eliminacion de particiones antiguas es instantanea (DROP TABLE)
  • Mantenimiento de indices mas rapido (indices por particion)

Mantenimiento:

-- Job mensual para crear nueva particion (2 meses adelante)
CREATE OR REPLACE FUNCTION create_next_audit_partition()
RETURNS VOID AS $$
DECLARE
    partition_date DATE;
    partition_name TEXT;
    start_date TEXT;
    end_date TEXT;
BEGIN
    partition_date := DATE_TRUNC('month', NOW() + INTERVAL '2 months')::DATE;
    partition_name := 'srv_audit_trail_' || TO_CHAR(partition_date, 'YYYY_MM');
    start_date := TO_CHAR(partition_date, 'YYYY-MM-DD');
    end_date := TO_CHAR(partition_date + INTERVAL '1 month', 'YYYY-MM-DD');

    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS %I PARTITION OF srv_audit_trail FOR VALUES FROM (%L) TO (%L)',
        partition_name, start_date, end_date
    );
END;
$$ LANGUAGE plpgsql;

-- Job mensual para eliminar particiones > 7 anos (compliance)
CREATE OR REPLACE FUNCTION drop_old_audit_partitions()
RETURNS VOID AS $$
DECLARE
    partition_record RECORD;
    partition_date DATE;
BEGIN
    FOR partition_record IN
        SELECT tablename FROM pg_tables
        WHERE tablename LIKE 'srv_audit_trail_%'
        ORDER BY tablename
    LOOP
        -- Extraer fecha de nombre de particion
        partition_date := TO_DATE(SUBSTRING(partition_record.tablename FROM 17), 'YYYY_MM');

        IF partition_date < NOW() - INTERVAL '7 years' THEN
            EXECUTE format('DROP TABLE IF EXISTS %I', partition_record.tablename);
            RAISE NOTICE 'Dropped partition: %', partition_record.tablename;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

6.3.3. srv_access_log (Servidor - Logs de Acceso)

Estrategia: Particionado porrango de tiempo (RANGE) semanal.

-- Tabla padre
CREATE TABLE srv_access_log (
    log_id          UUID NOT NULL,
    user_id         UUID NOT NULL,
    device_id       UUID NOT NULL,
    endpoint        VARCHAR(200) NOT NULL,
    method          VARCHAR(10) NOT NULL,
    status_code     INT NOT NULL,
    response_time_ms INT,
    ip_address      INET,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    PRIMARY KEY (log_id, created_at)
) PARTITION BY RANGE (created_at);

-- Particiones semanales (mas granular por alto volumen)
CREATE TABLE srv_access_log_2025_w01 PARTITION OF srv_access_log
    FOR VALUES FROM ('2025-01-01') TO ('2025-01-08');

-- Indices
CREATE INDEX idx_access_log_user_created ON srv_access_log(user_id, created_at);
CREATE INDEX idx_access_log_endpoint ON srv_access_log(endpoint);
CREATE INDEX idx_access_log_status ON srv_access_log(status_code) WHERE status_code >= 400;

Retention: 90 dias (mas corto que audit_trail). Particiones semanales permiten eliminacion precisa.

6.3.4. srv_ocr_requests (Servidor - Solicitudes OCR)

Estrategia: Particionado porhash de user_id (HASH).

-- Tabla padre
CREATE TABLE srv_ocr_requests (
    id                  UUID NOT NULL,
    user_id             UUID NOT NULL,
    prescription_id     UUID NOT NULL,
    anonymized_image    BYTEA,
    status              VARCHAR(20) NOT NULL,
    expires_at          TIMESTAMPTZ NOT NULL,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    PRIMARY KEY (id, user_id)
) PARTITION BY HASH (user_id);

-- 4 particiones (por hash de user_id)
CREATE TABLE srv_ocr_requests_p0 PARTITION OF srv_ocr_requests
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE srv_ocr_requests_p1 PARTITION OF srv_ocr_requests
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE srv_ocr_requests_p2 PARTITION OF srv_ocr_requests
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE srv_ocr_requests_p3 PARTITION OF srv_ocr_requests
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);

Por que HASH:

  • Distribucion uniforme de carga (no hay "hot partitions")
  • Queries siempre incluyen user_id (partition pruning)
  • Crecimiento balanceado entre particiones

Ventajas:

  • Cleanup job mas rapido (corre en paralelo por particion)
  • Indices mas pequenos por particion

6.4. Metricas de Monitoreo

6.4.1. Queries para Monitorear Particionado

-- Tamano de particiones
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE tablename LIKE 'srv_audit_trail_%'
   OR tablename LIKE 'srv_access_log_%'
   OR tablename LIKE 'srv_ocr_requests_%'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

-- Conteo de registros por particion
SELECT
    schemaname,
    tablename,
    n_tup_ins AS rows_inserted,
    n_tup_upd AS rows_updated,
    n_tup_del AS rows_deleted
FROM pg_stat_user_tables
WHERE tablename LIKE 'srv_audit_trail_%'
   OR tablename LIKE 'srv_access_log_%'
ORDER BY tablename;

-- Verificar partition pruning (queries usando particiones)
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM srv_audit_trail
WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01';
-- Debe mostrar solo "srv_audit_trail_2025_01" en el plan

6.4.2. Alertas

Metrica Umbral Accion
Particion > 10 GB Alerta Considerar particionado mas granular
Particiones faltantes (futuro) 2 meses Crear particiones automaticamente
Particiones antiguas sin eliminar > 7 anos (audit) / > 90 dias (access) Ejecutar cleanup job
Query sin partition pruning N/A Optimizar query para incluir filtro de fecha/user_id

6.5. Consideraciones de Implementacion

6.5.1. Fase de Implementacion

Fase Tabla Timeline Complejidad
v1.0 Ninguna N/A Volumetria aun baja
v1.1 srv_audit_trail 6 meses post-lanzamiento Baja (solo inserts)
v1.2 srv_access_log 9 meses post-lanzamiento Baja (solo inserts)
v2.0 srv_ocr_requests 12 meses post-lanzamiento Media (tiene updates)

6.5.2. Migracion de Tabla Existente a Particionada

-- Ejemplo: Migrar srv_audit_trail existente a particionada

-- 1. Renombrar tabla original
ALTER TABLE srv_audit_trail RENAME TO srv_audit_trail_old;

-- 2. Crear tabla particionada
CREATE TABLE srv_audit_trail (
    -- Schema igual a original
) PARTITION BY RANGE (created_at);

-- 3. Crear particiones para datos historicos
-- (por cada mes en tabla original)

-- 4. Copiar datos de tabla original a particionada
INSERT INTO srv_audit_trail
SELECT * FROM srv_audit_trail_old;

-- 5. Verificar conteo
SELECT COUNT(*) FROM srv_audit_trail;
SELECT COUNT(*) FROM srv_audit_trail_old;

-- 6. Eliminar tabla original (tras verificacion)
DROP TABLE srv_audit_trail_old;

Ventana de mantenimiento: 2-4 horas para 10M registros.

6.6. Decision Matrix: Cuando Particionar

Criterio Umbral Accion
Tamano de tabla > 100 GB Considerar particionado
Registros > 50M Considerar particionado
Crecimiento > 10M registros/mes Considerar particionado
Query pattern Siempre filtra por fecha/user_id Ideal para RANGE/HASH
Retention policy Elimina datos antiguos periodicamente Ideal para RANGE (drop partition)

NO particionar si:

  • Tabla < 10 GB
  • Queries escanean tabla completa (sin filtros)
  • Updates frecuentes en registros antiguos

7. Referencias Cruzadas

  • Arquitectura Cliente-Servidor: /02-arquitectura-cliente-servidor.md (ver sección 3.2.3.1 para Regla de 100 Registros)
  • Seguridad Cliente: /04-seguridad-cliente.md
  • Modelos de Base de Datos: /database/
  • APIs de Sincronización: /apis/sync/
  • APIs de Búsqueda de Catálogos: Ver /apis/catalogs/ para endpoints de búsqueda online
  • Políticas de Retención GDPR: /functional-spec/02-requisitos-regulatorios.md (Sección LGPD/GDPR)

Documento Generado: SpecQueen - 2025-12-07 Actualizado: ArchitectureDrone (Regla de 100 Registros) - 2025-12-08