Saltar a contenido

Modelo de Datos: Autenticacion

Identificador: MDL-AUTH-001 Version: 1.0.0 Fecha: 2025-12-07 Estado: Aprobado Autor: DatabaseDrone / SpecQueen Technical Division Modulo Funcional: MTS-AUTH-001



1. Introduccion

Este documento define el modelo de datos para el modulo de autenticacion de MedTime. Incluye tablas del servidor (PostgreSQL) y tablas del cliente (SQLite/Realm), respetando la arquitectura Zero-Knowledge.

1.1. Principios

Principio Aplicacion
Firebase Auth Autenticacion delegada a Firebase, servidor solo valida tokens
Zero-Knowledge Servidor no almacena credenciales ni datos de sesion sensibles
Blind Index Email y telefono almacenados como hash para lookup
RLS Row Level Security basado en Firebase UID

2. Entidades del Servidor

2.1. srv_users

Tabla principal de usuarios. Solo almacena metadata y blind indexes.

CREATE TABLE srv_users (
    -- Primary Key
    user_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Firebase Integration
    firebase_uid    VARCHAR(128) NOT NULL UNIQUE,

    -- Blind Indexes (SHA-256 hashes for lookup, NOT for authentication)
    email_hash      CHAR(64),           -- SHA-256 of lowercase email
    phone_hash      CHAR(64),           -- SHA-256 of E.164 phone

    -- User Classification
    role            VARCHAR(20) NOT NULL CHECK (role IN ('PI', 'PD', 'CS', 'CR')),
    tier            VARCHAR(20) NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'pro', 'perfect')),

    -- Audit Timestamps
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at      TIMESTAMPTZ,        -- Soft delete

    -- Constraints
    CONSTRAINT chk_cs_always_free CHECK (role != 'CS' OR tier = 'free'),
    CONSTRAINT chk_pd_no_direct_account CHECK (role != 'PD')  -- PD managed via cli_dependents
);

-- Indexes
CREATE UNIQUE INDEX idx_users_firebase_uid ON srv_users(firebase_uid);
CREATE INDEX idx_users_email_hash ON srv_users(email_hash) WHERE email_hash IS NOT NULL;
CREATE INDEX idx_users_phone_hash ON srv_users(phone_hash) WHERE phone_hash IS NOT NULL;
CREATE INDEX idx_users_role_tier ON srv_users(role, tier);
CREATE INDEX idx_users_deleted ON srv_users(deleted_at) WHERE deleted_at IS NOT NULL;

-- RLS Policy
ALTER TABLE srv_users ENABLE ROW LEVEL SECURITY;

CREATE POLICY users_self_access ON srv_users
    FOR ALL
    USING (firebase_uid = current_setting('app.firebase_uid', true));

CREATE POLICY users_admin_access ON srv_users
    FOR ALL
    USING (current_setting('app.is_admin', true) = 'true');

2.1.1. Columnas

Columna Tipo Nullable Descripcion
user_id UUID No Identificador unico interno
firebase_uid VARCHAR(128) No UID de Firebase Authentication
email_hash CHAR(64) Si SHA-256 del email (blind index)
phone_hash CHAR(64) Si SHA-256 del telefono E.164
role VARCHAR(20) No Rol: PI, PD, CS, CR
tier VARCHAR(20) No Tier: free, pro, perfect
created_at TIMESTAMPTZ No Fecha de creacion
updated_at TIMESTAMPTZ No Ultima actualizacion
deleted_at TIMESTAMPTZ Si Fecha de borrado (soft delete)

2.1.2. Reglas de Negocio

Regla Constraint Descripcion
RN-AUTH-001 chk_cs_always_free CS siempre tiene tier=free
RN-AUTH-002 chk_pd_no_direct_account PD no tiene cuenta directa (gestionado por CR)

2.2. srv_sessions

Registro de sesiones activas. El servidor solo almacena metadata, no tokens.

CREATE TABLE srv_sessions (
    -- Primary Key
    session_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Foreign Keys
    user_id         UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    device_id       UUID REFERENCES srv_devices(device_id) ON DELETE SET NULL,

    -- Session Metadata
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ NOT NULL,
    last_activity   TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Revocation
    revoked_at      TIMESTAMPTZ,
    revoked_reason  VARCHAR(100),

    -- Security (hashed, not actual values)
    ip_hash         CHAR(64),           -- SHA-256 of IP address
    user_agent_hash CHAR(64),           -- SHA-256 of User-Agent

    -- Session Type
    mfa_verified    BOOLEAN NOT NULL DEFAULT FALSE,

    -- Constraints
    CONSTRAINT chk_session_dates CHECK (expires_at > created_at)
);

-- Indexes
CREATE INDEX idx_sessions_user ON srv_sessions(user_id);
CREATE INDEX idx_sessions_device ON srv_sessions(device_id);
CREATE INDEX idx_sessions_expires ON srv_sessions(expires_at) WHERE revoked_at IS NULL;
CREATE INDEX idx_sessions_active ON srv_sessions(user_id, revoked_at) WHERE revoked_at IS NULL;
CREATE INDEX idx_sessions_last_activity ON srv_sessions(last_activity) WHERE revoked_at IS NULL;

-- RLS Policy
ALTER TABLE srv_sessions ENABLE ROW LEVEL SECURITY;

CREATE POLICY sessions_self_access ON srv_sessions
    FOR ALL
    USING (user_id IN (
        SELECT user_id FROM srv_users
        WHERE firebase_uid = current_setting('app.firebase_uid', true)
    ));

2.2.1. Columnas

Columna Tipo Nullable Descripcion
session_id UUID No Identificador unico de sesion
user_id UUID No FK a srv_users
device_id UUID Si FK a srv_devices
created_at TIMESTAMPTZ No Inicio de sesion
expires_at TIMESTAMPTZ No Expiracion de sesion
last_activity TIMESTAMPTZ No Ultima actividad
revoked_at TIMESTAMPTZ Si Fecha de revocacion
revoked_reason VARCHAR(100) Si Razon de revocacion
ip_hash CHAR(64) Si Hash de IP (auditoria)
user_agent_hash CHAR(64) Si Hash de User-Agent
mfa_verified BOOLEAN No Si MFA fue verificado

2.2.2. Duracion de Sesion por Tier

Tier Duracion Max Dispositivos
free N/A (local only) 1
pro 30 dias 3
perfect 7 dias 5

2.3. srv_devices

Dispositivos registrados por usuario.

CREATE TABLE srv_devices (
    -- Primary Key
    device_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Foreign Key
    user_id         UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,

    -- Device Info (encrypted where sensitive)
    device_name_enc BYTEA,              -- Encrypted device name
    platform        VARCHAR(20) NOT NULL CHECK (platform IN ('ios', 'android', 'web')),
    os_version      VARCHAR(50),
    app_version     VARCHAR(20),

    -- Push Notifications (encrypted token)
    push_token_enc  BYTEA,              -- Encrypted FCM/APNs token
    push_enabled    BOOLEAN NOT NULL DEFAULT TRUE,

    -- Sync State
    last_sync_at    TIMESTAMPTZ,
    sync_version    BIGINT NOT NULL DEFAULT 0,

    -- Audit
    registered_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_seen_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deactivated_at  TIMESTAMPTZ,

    -- Constraints
    CONSTRAINT chk_device_platform CHECK (platform IN ('ios', 'android', 'web'))
);

-- Indexes
CREATE INDEX idx_devices_user ON srv_devices(user_id);
CREATE INDEX idx_devices_platform ON srv_devices(platform);
CREATE INDEX idx_devices_active ON srv_devices(user_id, deactivated_at) WHERE deactivated_at IS NULL;
CREATE INDEX idx_devices_sync ON srv_devices(user_id, last_sync_at);

-- RLS Policy
ALTER TABLE srv_devices ENABLE ROW LEVEL SECURITY;

CREATE POLICY devices_self_access ON srv_devices
    FOR ALL
    USING (user_id IN (
        SELECT user_id FROM srv_users
        WHERE firebase_uid = current_setting('app.firebase_uid', true)
    ));

2.3.1. Columnas

Columna Tipo Nullable Cifrado Descripcion
device_id UUID No No Identificador unico
user_id UUID No No FK a srv_users
device_name_enc BYTEA Si Si Nombre del dispositivo cifrado
platform VARCHAR(20) No No ios, android, web
os_version VARCHAR(50) Si No Version del SO
app_version VARCHAR(20) Si No Version de la app
push_token_enc BYTEA Si Si Token push cifrado
push_enabled BOOLEAN No No Si push esta habilitado
last_sync_at TIMESTAMPTZ Si No Ultima sincronizacion
sync_version BIGINT No No Version de sync
registered_at TIMESTAMPTZ No No Fecha de registro
last_seen_at TIMESTAMPTZ No No Ultima actividad
deactivated_at TIMESTAMPTZ Si No Fecha de desactivacion

2.4. srv_user_relations

Relaciones entre usuarios (cuidador-paciente).

CREATE TABLE srv_user_relations (
    -- Primary Key
    relation_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Foreign Keys
    owner_user_id   UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    related_user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,

    -- Relation Type
    relation_type   VARCHAR(20) NOT NULL CHECK (relation_type IN ('caregiver_of', 'patient_of')),

    -- Permissions (encrypted JSON blob)
    permissions_enc BYTEA,              -- Encrypted permissions object

    -- Audit
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by      UUID REFERENCES srv_users(user_id),
    revoked_at      TIMESTAMPTZ,
    revoked_by      UUID REFERENCES srv_users(user_id),

    -- Constraints
    CONSTRAINT chk_no_self_relation CHECK (owner_user_id != related_user_id),
    CONSTRAINT uq_relation UNIQUE (owner_user_id, related_user_id, relation_type)
);

-- Indexes
CREATE INDEX idx_relations_owner ON srv_user_relations(owner_user_id);
CREATE INDEX idx_relations_related ON srv_user_relations(related_user_id);
CREATE INDEX idx_relations_active ON srv_user_relations(owner_user_id, revoked_at) WHERE revoked_at IS NULL;

-- RLS Policy
ALTER TABLE srv_user_relations ENABLE ROW LEVEL SECURITY;

CREATE POLICY relations_participant_access ON srv_user_relations
    FOR ALL
    USING (
        owner_user_id IN (SELECT user_id FROM srv_users WHERE firebase_uid = current_setting('app.firebase_uid', true))
        OR
        related_user_id IN (SELECT user_id FROM srv_users WHERE firebase_uid = current_setting('app.firebase_uid', true))
    );

2.4.1. Columnas

Columna Tipo Nullable Cifrado Descripcion
relation_id UUID No No Identificador unico
owner_user_id UUID No No Usuario propietario de la relacion
related_user_id UUID No No Usuario relacionado
relation_type VARCHAR(20) No No Tipo: caregiver_of, patient_of
permissions_enc BYTEA Si Si Permisos granulares cifrados
created_at TIMESTAMPTZ No No Fecha de creacion
created_by UUID Si No Quien creo la relacion
revoked_at TIMESTAMPTZ Si No Fecha de revocacion
revoked_by UUID Si No Quien revoco

2.4.2. Estructura de Permissions (Descifrado en Cliente)

{
  "ver_medicamentos": true,
  "ver_adherencia": true,
  "registrar_tomas": false,
  "recibir_alertas_tomas": true,
  "ver_recetas": false,
  "ver_citas": false,
  "ver_analisis": false
}

2.5. srv_invitations

Invitaciones pendientes para cuidadores solidarios.

CREATE TABLE srv_invitations (
    -- Primary Key
    invitation_id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Foreign Key
    inviter_user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,

    -- Invitee (blind index, no PII)
    invitee_email_hash CHAR(64),        -- SHA-256 of email
    invitee_phone_hash CHAR(64),        -- SHA-256 of phone

    -- Token (hashed, actual token sent via email/sms)
    token_hash      CHAR(64) NOT NULL,  -- SHA-256 of invitation token

    -- Status
    status          VARCHAR(20) NOT NULL DEFAULT 'pending'
                    CHECK (status IN ('pending', 'accepted', 'declined', 'expired', 'cancelled')),

    -- Permissions offered (encrypted)
    permissions_enc BYTEA,

    -- Expiration
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ NOT NULL,
    responded_at    TIMESTAMPTZ,

    -- Accepted user (if accepted)
    accepted_user_id UUID REFERENCES srv_users(user_id),

    -- Constraints
    CONSTRAINT chk_has_contact CHECK (invitee_email_hash IS NOT NULL OR invitee_phone_hash IS NOT NULL),
    CONSTRAINT chk_expiry CHECK (expires_at > created_at)
);

-- Indexes
CREATE INDEX idx_invitations_inviter ON srv_invitations(inviter_user_id);
CREATE INDEX idx_invitations_token ON srv_invitations(token_hash);
CREATE INDEX idx_invitations_email ON srv_invitations(invitee_email_hash) WHERE invitee_email_hash IS NOT NULL;
CREATE INDEX idx_invitations_pending ON srv_invitations(status, expires_at) WHERE status = 'pending';

-- RLS Policy
ALTER TABLE srv_invitations ENABLE ROW LEVEL SECURITY;

CREATE POLICY invitations_inviter_access ON srv_invitations
    FOR ALL
    USING (inviter_user_id IN (
        SELECT user_id FROM srv_users
        WHERE firebase_uid = current_setting('app.firebase_uid', true)
    ));

2.5.1. Columnas

Columna Tipo Nullable Descripcion
invitation_id UUID No Identificador unico
inviter_user_id UUID No FK al usuario que invita
invitee_email_hash CHAR(64) Si Hash del email del invitado
invitee_phone_hash CHAR(64) Si Hash del telefono
token_hash CHAR(64) No Hash del token de invitacion
status VARCHAR(20) No Estado de la invitacion
permissions_enc BYTEA Si Permisos ofrecidos (cifrados)
created_at TIMESTAMPTZ No Fecha de creacion
expires_at TIMESTAMPTZ No Fecha de expiracion
responded_at TIMESTAMPTZ Si Fecha de respuesta
accepted_user_id UUID Si FK al usuario que acepto

3. Entidades del Cliente

3.1. cli_auth_state (LOCAL_ONLY)

Estado de autenticacion local.

// iOS - Swift/Realm
class AuthState: Object {
    @Persisted(primaryKey: true) var id: String = "auth_state"

    // User Info (decrypted locally)
    @Persisted var userId: String?
    @Persisted var firebaseUid: String?
    @Persisted var email: String?
    @Persisted var displayName: String?
    @Persisted var role: String?          // PI, CS, CR
    @Persisted var tier: String?          // free, pro, perfect

    // Session
    @Persisted var isAuthenticated: Bool = false
    @Persisted var sessionExpiresAt: Date?
    @Persisted var mfaVerified: Bool = false

    // Local Security
    @Persisted var pinHash: String?       // Argon2 hash of PIN
    @Persisted var biometricEnabled: Bool = false
    @Persisted var failedAttempts: Int = 0
    @Persisted var lockedUntil: Date?

    // Sync State
    @Persisted var lastSyncAt: Date?
    @Persisted var syncVersion: Int64 = 0
}
// Android - Kotlin/Room
@Entity(tableName = "cli_auth_state")
data class AuthState(
    @PrimaryKey
    val id: String = "auth_state",

    // User Info
    val userId: String?,
    val firebaseUid: String?,
    val email: String?,
    val displayName: String?,
    val role: String?,
    val tier: String?,

    // Session
    val isAuthenticated: Boolean = false,
    val sessionExpiresAt: Long?, // Unix timestamp
    val mfaVerified: Boolean = false,

    // Local Security
    val pinHash: String?,
    val biometricEnabled: Boolean = false,
    val failedAttempts: Int = 0,
    val lockedUntil: Long?,

    // Sync State
    val lastSyncAt: Long?,
    val syncVersion: Long = 0
)

3.1.1. Clasificacion: LOCAL_ONLY

Esta entidad nunca se sincroniza. Contiene:

  • Estado de sesion local
  • Hash del PIN (Argon2)
  • Configuracion biometrica
  • Contador de intentos fallidos

3.2. cli_keychain_items (LOCAL_ONLY)

Referencia a items almacenados en Keychain/Keystore.

Key Descripcion Proteccion
medtime.master_key Master Key derivada de credenciales Secure Enclave / StrongBox
medtime.firebase_token Token de Firebase (Pro/Perfect) Biometric / PIN
medtime.encryption_salt Salt para derivacion de claves Device-bound
medtime.recovery_key_enc Recovery key cifrada (Perfect) Biometric

4. Funciones de Base de Datos

4.1. Funcion: Crear Usuario desde Firebase

CREATE OR REPLACE FUNCTION create_user_from_firebase(
    p_firebase_uid VARCHAR(128),
    p_email VARCHAR(255),
    p_phone VARCHAR(20),
    p_role VARCHAR(20),
    p_tier VARCHAR(20)
) RETURNS UUID AS $$
DECLARE
    v_user_id UUID;
    v_email_hash CHAR(64);
    v_phone_hash CHAR(64);
BEGIN
    -- Generate blind indexes
    IF p_email IS NOT NULL THEN
        v_email_hash := encode(sha256(lower(p_email)::bytea), 'hex');
    END IF;

    IF p_phone IS NOT NULL THEN
        v_phone_hash := encode(sha256(p_phone::bytea), 'hex');
    END IF;

    -- Enforce CS always free
    IF p_role = 'CS' AND p_tier != 'free' THEN
        RAISE EXCEPTION 'Cuidador Solidario must always be free tier';
    END IF;

    -- Insert user
    INSERT INTO srv_users (firebase_uid, email_hash, phone_hash, role, tier)
    VALUES (p_firebase_uid, v_email_hash, v_phone_hash, p_role, COALESCE(p_tier, 'free'))
    RETURNING user_id INTO v_user_id;

    -- Audit
    INSERT INTO srv_audit_trail (user_id, entity_type, entity_id_hash, action, timestamp)
    VALUES (v_user_id, 'user', encode(sha256(v_user_id::text::bytea), 'hex'), 'CREATE', NOW());

    RETURN v_user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

4.2. Funcion: Verificar Limite de Dispositivos

CREATE OR REPLACE FUNCTION check_device_limit(p_user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
    v_tier VARCHAR(20);
    v_device_count INT;
    v_max_devices INT;
BEGIN
    -- Get user tier
    SELECT tier INTO v_tier FROM srv_users WHERE user_id = p_user_id;

    -- Get current device count
    SELECT COUNT(*) INTO v_device_count
    FROM srv_devices
    WHERE user_id = p_user_id AND deactivated_at IS NULL;

    -- Set limit based on tier
    v_max_devices := CASE v_tier
        WHEN 'free' THEN 1
        WHEN 'pro' THEN 3
        WHEN 'perfect' THEN 5
        ELSE 1
    END;

    RETURN v_device_count < v_max_devices;
END;
$$ LANGUAGE plpgsql;

4.3. Funcion: Revocar Sesiones Antiguas

CREATE OR REPLACE FUNCTION revoke_oldest_sessions(
    p_user_id UUID,
    p_keep_count INT DEFAULT 1
) RETURNS INT AS $$
DECLARE
    v_revoked INT;
BEGIN
    WITH sessions_to_revoke AS (
        SELECT session_id
        FROM srv_sessions
        WHERE user_id = p_user_id
          AND revoked_at IS NULL
        ORDER BY created_at DESC
        OFFSET p_keep_count
    )
    UPDATE srv_sessions
    SET revoked_at = NOW(),
        revoked_reason = 'device_limit_exceeded'
    WHERE session_id IN (SELECT session_id FROM sessions_to_revoke);

    GET DIAGNOSTICS v_revoked = ROW_COUNT;
    RETURN v_revoked;
END;
$$ LANGUAGE plpgsql;

5. Migraciones

5.1. Migration 001: Create Auth Schema

-- Migration: 001_create_auth_schema
-- Date: 2025-12-07
-- Author: DatabaseDrone

BEGIN;

-- Create users table
CREATE TABLE IF NOT EXISTS srv_users (
    user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    firebase_uid VARCHAR(128) NOT NULL UNIQUE,
    email_hash CHAR(64),
    phone_hash CHAR(64),
    role VARCHAR(20) NOT NULL CHECK (role IN ('PI', 'PD', 'CS', 'CR')),
    tier VARCHAR(20) NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'pro', 'perfect')),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at TIMESTAMPTZ,
    CONSTRAINT chk_cs_always_free CHECK (role != 'CS' OR tier = 'free'),
    CONSTRAINT chk_pd_no_direct_account CHECK (role != 'PD')
);

-- Create sessions table
CREATE TABLE IF NOT EXISTS srv_sessions (
    session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    device_id UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL,
    last_activity TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    revoked_at TIMESTAMPTZ,
    revoked_reason VARCHAR(100),
    ip_hash CHAR(64),
    user_agent_hash CHAR(64),
    mfa_verified BOOLEAN NOT NULL DEFAULT FALSE,
    CONSTRAINT chk_session_dates CHECK (expires_at > created_at)
);

-- Create devices table
CREATE TABLE IF NOT EXISTS srv_devices (
    device_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    device_name_enc BYTEA,
    platform VARCHAR(20) NOT NULL CHECK (platform IN ('ios', 'android', 'web')),
    os_version VARCHAR(50),
    app_version VARCHAR(20),
    push_token_enc BYTEA,
    push_enabled BOOLEAN NOT NULL DEFAULT TRUE,
    last_sync_at TIMESTAMPTZ,
    sync_version BIGINT NOT NULL DEFAULT 0,
    registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deactivated_at TIMESTAMPTZ
);

-- Add foreign key for sessions -> devices (after devices created)
ALTER TABLE srv_sessions
    ADD CONSTRAINT fk_sessions_device
    FOREIGN KEY (device_id) REFERENCES srv_devices(device_id) ON DELETE SET NULL;

-- Create user_relations table
CREATE TABLE IF NOT EXISTS srv_user_relations (
    relation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    owner_user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    related_user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    relation_type VARCHAR(20) NOT NULL CHECK (relation_type IN ('caregiver_of', 'patient_of')),
    permissions_enc BYTEA,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID REFERENCES srv_users(user_id),
    revoked_at TIMESTAMPTZ,
    revoked_by UUID REFERENCES srv_users(user_id),
    CONSTRAINT chk_no_self_relation CHECK (owner_user_id != related_user_id),
    CONSTRAINT uq_relation UNIQUE (owner_user_id, related_user_id, relation_type)
);

-- Create invitations table
CREATE TABLE IF NOT EXISTS srv_invitations (
    invitation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    inviter_user_id UUID NOT NULL REFERENCES srv_users(user_id) ON DELETE CASCADE,
    invitee_email_hash CHAR(64),
    invitee_phone_hash CHAR(64),
    token_hash CHAR(64) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending'
        CHECK (status IN ('pending', 'accepted', 'declined', 'expired', 'cancelled')),
    permissions_enc BYTEA,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at TIMESTAMPTZ NOT NULL,
    responded_at TIMESTAMPTZ,
    accepted_user_id UUID REFERENCES srv_users(user_id),
    CONSTRAINT chk_has_contact CHECK (invitee_email_hash IS NOT NULL OR invitee_phone_hash IS NOT NULL),
    CONSTRAINT chk_expiry CHECK (expires_at > created_at)
);

-- Create all indexes
CREATE UNIQUE INDEX idx_users_firebase_uid ON srv_users(firebase_uid);
CREATE INDEX idx_users_email_hash ON srv_users(email_hash) WHERE email_hash IS NOT NULL;
CREATE INDEX idx_users_phone_hash ON srv_users(phone_hash) WHERE phone_hash IS NOT NULL;
CREATE INDEX idx_users_role_tier ON srv_users(role, tier);

CREATE INDEX idx_sessions_user ON srv_sessions(user_id);
CREATE INDEX idx_sessions_device ON srv_sessions(device_id);
CREATE INDEX idx_sessions_expires ON srv_sessions(expires_at) WHERE revoked_at IS NULL;
CREATE INDEX idx_sessions_last_activity ON srv_sessions(last_activity) WHERE revoked_at IS NULL;

CREATE INDEX idx_devices_user ON srv_devices(user_id);
CREATE INDEX idx_devices_active ON srv_devices(user_id, deactivated_at) WHERE deactivated_at IS NULL;

CREATE INDEX idx_relations_owner ON srv_user_relations(owner_user_id);
CREATE INDEX idx_relations_related ON srv_user_relations(related_user_id);

CREATE INDEX idx_invitations_inviter ON srv_invitations(inviter_user_id);
CREATE INDEX idx_invitations_token ON srv_invitations(token_hash);

COMMIT;

6. Referencias Cruzadas

Documento Relacion
DB-ERD-001 Diagrama ER completo
MTS-AUTH-001 Especificacion funcional
05-seguridad-servidor.md RLS y auditoria
04-seguridad-cliente.md Keychain/Keystore
INV-015 Firebase Auth docs
INV-016 Firebase Admin SDK

Documento generado por DatabaseDrone - SpecQueen Technical Division "La autenticacion segura es la primera linea de defensa."