---
{
  "title": "Seudonimización HIPAA Safe Harbor en Aidbox: De FHIR a análisis sin exponer datos de pacientes",
  "description": "Aidbox permite la seudonimización HIPAA Safe Harbor directamente en ViewDefinitions. Transforme datos FHIR en tablas conformes y listas para análisis con control por columna, y recupere los resultados originales cuando sea necesario.",
  "date": "2026-04-22",
  "author": "Andrew Listopadov",
  "reading-time": "10 min read",
  "tags": [
    "SQL on FHIR",
    "Compliance",
    "Analytics",
    "Aidbox"
  ],
  "seo-tags": [
    "HIPAA Safe Harbor",
    "FHIR de-identification",
    "SQL on FHIR",
    "healthcare data analytics",
    "PHI protection",
    "ViewDefinition"
  ]
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---
## El problema con el análisis de datos sanitarios

Las organizaciones sanitarias disponen de datos valiosos, pero compartirlos es un campo minado. Un hospital quiere estudiar las tasas de lesión renal aguda en su población de UCI. Un equipo de investigación externo tiene la experiencia estadística necesaria. Los datos existen — cientos de miles de observaciones de laboratorio almacenadas como recursos FHIR en Aidbox. Pero entregar registros de pacientes sin procesar viola HIPAA.

La solución habitual es un pipeline ETL a medida: exportar datos, ejecutar un script para eliminar los identificadores, confiar en que nada se escape, y cargar los datos depurados en otro lugar. Esto es lento, propenso a errores y difícil de auditar. Cada nueva pregunta de investigación implica otro pipeline.

¿Y si la seudonimización ocurriera *dentro* de la base de datos, declarada junto a la vista que da forma a los datos?

## La seudonimización como característica de ViewDefinition

Aidbox ahora admite la seudonimización por columna en [ViewDefinitions](/blog/what-is-a-viewdefinition). Se anotan las columnas con una [extensión FHIR](/articles/extending-fhir-resources) que especifica qué transformación aplicar. El compilador SQL envuelve cada expresión de columna con una función de PostgreSQL — los datos sensibles se transforman antes de llegar a la salida.

```json
{
  "resourceType": "ViewDefinition",
  "name": "deident_observations",
  "status": "active",
  "resource": "Observation",
  "select": [{
    "column": [
      {
        "name": "patient_id",
        "path": "subject.getReferenceKey(Patient)",
        "extension": [{
          "url": "http://health-samurai.io/fhir/core/StructureDefinition/de-identification",
          "extension": [
            {"url": "method", "valueCode": "cryptoHash"},
            {"url": "cryptoHashKey", "valueString": "my-secret-key"}
          ]
        }]
      },
      {
        "name": "status",
        "path": "status"
      }
    ]
  }]
}
```

La columna `patient_id` lleva una extensión de seudonimización. Generará un hash HMAC-SHA256 en lugar de la referencia real del paciente — determinista (el mismo paciente siempre obtiene el mismo hash) e irreversible (no es posible recuperar el valor original a partir del hash). La columna `status` no tiene extensión y se transfiere sin cambios.

El constructor de ViewDefinition en la interfaz de Aidbox hace esto visual — haga clic en el icono de escudo en cualquier columna para elegir un método y configurar los parámetros:

![Set up de-identification in ViewDefinition Builder](image-1.png)

## Métodos disponibles

| Método                  | Qué hace                                                              | Parámetros clave                           |
|-------------------------|-----------------------------------------------------------------------|--------------------------------------------|
| **redact**              | Reemplaza con NULL                                                    | —                                          |
| **cryptoHash**          | Hash HMAC-SHA256 (determinista, unidireccional)                       | `cryptoHashKey`                            |
| **dateshift**           | Desplaza fechas ±1–50 días por recurso                                | `dateShiftKey`                             |
| **birthDateSafeHarbor** | Desplazamiento de fecha + redacción automática si edad >89 (solo Patient.birthDate) | `dateShiftKey`                |
| **encrypt**             | AES-128-CBC, salida en base64 (reversible con clave)                  | `encryptKey`                               |
| **substitute**          | Reemplaza con una cadena fija                                         | `replaceWith`                              |
| **perturb**             | Añade ruido aleatorio a valores numéricos                             | `span`, `rangeType`, `roundTo`             |
| **custom_function**     | Llama a su propia función de PostgreSQL                               | `custom_function`, `custom_arg` (opcional) |

Dos métodos son especialmente relevantes para el patrón que vamos a explorar:

**cryptoHash** produce la misma salida para la misma entrada — lo que significa que puede usarlo para unir tablas. Aplique el hash al ID del paciente en dos ViewDefinitions distintas con la misma clave, y los hashes coincidirán. Esta es la base para la reidentificación.

**dateshift** desplaza todas las fechas dentro del mismo recurso con el mismo desplazamiento, preservando las relaciones temporales. Una diferencia de 3 días entre dos eventos sigue siendo de 3 días. El desplazamiento se deriva de `HMAC(dateShiftKey, resource.id)`, por lo que es determinista por recurso pero impredecible sin la clave.

## Ejemplo práctico: subcontratar la investigación de lesión renal aguda

Recorramos un escenario realista usando datos MIMIC-IV cargados en Aidbox — 100 pacientes de UCI, más de 800.000 observaciones.

**El objetivo**: Un hospital quiere que un equipo de investigación externo analice las tasas de lesión renal aguda (AKI) en su población de UCI. Los investigadores necesitan resultados de creatinina en laboratorio, pero no deben ver los identificadores reales de los pacientes ni las fechas exactas.

**El enfoque**: Crear dos tablas materializadas — un conjunto de datos seudonimizado para los investigadores, y una tabla de mapeo interna que conecta los IDs hasheados con los pacientes reales.

### Diseño de la vista seudonimizada

Comenzamos con una ViewDefinition sobre el recurso `Observation`, filtrada a resultados de laboratorio de creatinina. Cuatro columnas, cada una con una estrategia de seudonimización diferente:

**ID del paciente** — la columna que más protección necesita. Aplicamos `cryptoHash` para convertir el UUID real en una cadena hexadecimal HMAC-SHA256 irreversible:

```json
// ---8<--- snip
{
  "name": "patient_id",
  "path": "subject.getReferenceKey(Patient)",
  "extension": [{
    "url": "..de-identification",
    "extension": [
      {"url": "method", "valueCode": "cryptoHash"},
      {"url": "cryptoHashKey", "valueString": "aki-study-2026"}
    ]
  }]
}
// ---8<--- snap
```

**Valor de creatinina y unidad** — los datos clínicos que el investigador realmente necesita. Sin seudonimización — los valores de laboratorio no son identificadores:

```json
// ---8<--- snip
{
  "name": "creatinine",
  "path": "value.ofType(Quantity).value",
  "type": "decimal"
},
{
  "name": "unit",
  "path": "value.ofType(Quantity).unit"
}
// ---8<--- snap
```

**Fecha efectiva** — desplazada por un desplazamiento determinista por recurso usando `dateshift`. Una diferencia de 3 días entre dos análisis sigue siendo de 3 días, pero la fecha real del calendario queda oculta:

```json
// ---8<--- snip
{
  "name": "effective_date",
  "path": "effective.ofType(dateTime)",
  "extension": [{
    "url": "..de-identification",
    "extension": [
      {"url": "method", "valueCode": "dateshift"},
      {"url": "dateShiftKey", "valueString": "aki-study-2026"}
    ]
  }]
}
// ---8<--- snap
```

### La tabla de mapeo

La segunda ViewDefinition es para la reidentificación — permanece de uso interno. Dos columnas sobre el recurso Patient, ambas apuntando a `id`: una en texto plano y otra hasheada con la **misma clave** que la anterior. Como `cryptoHash` es determinista, los IDs hasheados coincidirán entre ambas tablas.

### Construcción en la interfaz

El constructor de ViewDefinition hace esto visual. Haga clic en el icono de escudo en cualquier columna para elegir un método de seudonimización y configurar sus parámetros:

![Create the ViewDefinition in the builder interface and configure de-identification methods on each column](image-2.png)

Una vez guardado, materialice como tabla:

![Materialize the ViewDefinition as a table](image-3.png)

{% hint style="info" %}
Las ViewDefinitions con extensiones de seudonimización solo pueden materializarse como `table`, no como `view` ni `materialized-view`. PostgreSQL almacena la definición SQL completa de las vistas en los catálogos del sistema (`pg_views`), lo que expondría sus claves criptográficas a cualquier persona con acceso al catálogo. Las tablas almacenan únicamente los datos transformados.
{% endhint %}

### Lo que ve el investigador

El investigador consulta la tabla seudonimizada. Sin IDs de paciente reales, sin fechas reales:

```sql
SELECT * FROM sof.deident_creatinine LIMIT 5;
```

| `patient_id`               | `creatinine` | `unit` | `effective_date`          |
|----------------------------|-------------:|--------|---------------------------|
| db1d98d58...f1508c7b439aba |          1.1 | mg/dL  | 2113-01-17T09:45:00-05:00 |
| db1d98d58...f1508c7b439aba |          1.4 | mg/dL  | 2116-03-05T16:16:00-05:00 |
| f852c0e57...ca9c6e3db462a9 |          0.8 | mg/dL  | 2153-03-08T02:17:00-04:00 |
| 59e60d080...01688175f47b16 |          1.7 | mg/dL  | 2147-09-09T15:20:00-04:00 |
| 6283390bd...738cd02dac4c08 |          0.8 | mg/dL  | 2140-10-03T15:20:00-04:00 |

El `patient_id` es una cadena hexadecimal — el investigador puede agrupar por ella, contar pacientes distintos y seguir tendencias a lo largo del tiempo. Pero no puede revertirla para encontrar al paciente real.

### Cálculo de la métrica

La lesión renal aguda se identifica habitualmente por la elevación de creatinina. Un criterio de cribado simplificado: creatinina superior a 1,5 mg/dL indica posible daño renal. El investigador ejecuta:

```sql
SELECT
  CASE
    WHEN creatinine <= 1.1 THEN 'Normal (≤1.1)'
    WHEN creatinine <= 1.5 THEN 'Elevated (1.1–1.5)'
    WHEN creatinine <= 3.0 THEN 'High (1.5–3.0)'
    ELSE 'Critical (>3.0)'
  END AS risk_category,
  count(*) AS observation_count,
  count(DISTINCT patient_id) AS patient_count,
  round(avg(creatinine)::numeric, 2) AS avg_creatinine
FROM sof.deident_creatinine
GROUP BY 1
ORDER BY 1;
```

| `risk_category`    | `observation_count` | `patient_count` | `avg_creatinine` |
|--------------------|--------------------:|----------------:|-----------------:|
| Critical (>3.0)    |                 307 |              19 |             5.08 |
| Elevated (1.1–1.5) |                 393 |              40 |             1.33 |
| High (1.5–3.0)     |                 575 |              30 |             2.11 |
| Normal (≤1.1)      |                1728 |              84 |             0.77 |

19 pacientes tuvieron lecturas de creatinina superiores a 3,0 mg/dL — un umbral crítico que sugiere lesión renal grave. El investigador identifica a estos pacientes por sus IDs hasheados y envía los resultados al hospital.

### Reidentificación para seguimiento clínico

El hospital une los resultados de la investigación con la tabla de mapeo interna — haciendo coincidir los IDs hasheados con los UUIDs reales de los pacientes:

```sql
SELECT
  m.real_id,
  max(o.creatinine) AS peak_creatinine,
  count(*) AS high_readings
FROM sof.deident_creatinine o
JOIN sof.patient_id_map m ON o.patient_id = m.hashed_id
WHERE o.creatinine > 3.0
GROUP BY m.real_id
ORDER BY peak_creatinine DESC
LIMIT 20;
```

| `real_id`                            | `peak_creatinine` | `high_readings` |
|--------------------------------------|------------------:|----------------:|
| af0e5009-d87d-52a8-ac8a-676e471c41f1 |              15.2 |               9 |
| 8e77dd0b-932d-5790-9ba6-5c6df8434457 |                11 |              80 |
| 1cf9e585-806c-513b-80af-4ca565a28231 |               9.1 |              62 |
| 568cb149-804c-59e8-bdf5-816e8151cd22 |               8.3 |               9 |
| df756e08-6ea8-5d69-b918-67911945f827 |               7.9 |              11 |
| 7ec7078a-0593-5a99-9862-ebbff47fd1c5 |               6.4 |              20 |
| dd2bf984-33c3-5874-8f68-84113327877e |               6.2 |              26 |
| 4f773083-7f4d-5378-b839-c24ca1e15434 |               5.3 |              11 |
| 72d56b49-a7ee-5b9a-a679-25d1c836d3c3 |               5.1 |               3 |
| ...                                  |               ... |             ... |
| 842680b3-e421-58cc-8050-3b29668b438c |               3.2 |               1 |

Ahora el hospital sabe exactamente qué pacientes necesitan atención clínica — sin haber expuesto nunca sus identidades al equipo de investigación.

### El panorama completo

```mermaid
graph TD
  A[FHIR Observations in Aidbox] -->| ViewDefinition + de-identification | B[De-identified table]
  A -->| ViewDefinition ID mapping | C[Mapping table]
  B -->| shared with | D[External researcher]
  D -->| results with hashed IDs | E[Hospital receives results]
  C -->| stays internal | E
  E -->| JOIN on mapping table | F[Real patient IDs for follow-up]
```

La idea clave: **la misma `cryptoHashKey` lo une todo**. El investigador trabaja exclusivamente con identificadores hasheados. El hospital guarda la tabla de mapeo y la clave. La reidentificación ocurre solo de forma interna y solo cuando es necesario.

## ViewDefinitions Safe Harbor predefinidas

Escribir reglas de seudonimización para cada tipo de recurso es tedioso. Proporcionamos [`io.health-samurai.de-identification.r4`](https://get-ig.org/io.health-samurai.de-identification.r4) — un paquete FHIR disponible a través del registro de artefactos de Aidbox con ViewDefinitions predefinidas para 17 tipos de recursos R4 habituales: Patient, Encounter, Observation, Condition, Claim, ExplanationOfBenefit, AllergyIntolerance, DiagnosticReport, MedicationRequest, MedicationDispense, MedicationAdministration, Immunization, Procedure, Specimen, DocumentReference, Practitioner y Location.

Cada ViewDefinition aplica las reglas Safe Harbor:

- **cryptoHash** en identificadores y referencias — coherente entre tablas para uniones
- **dateshift** en fechas clínicas — preserva las relaciones temporales
- **birthDateSafeHarbor** en Patient.birthDate — desplazamiento de fecha más redacción automática cuando el paciente tiene más de 89 años, según lo exigido por [45 CFR 164.514(b)(2)(i)(C)](https://www.law.cornell.edu/cfr/text/45/section-164.514)
- **redact** en nombres, direcciones y otros identificadores directos

Todos los parámetros de clave criptográfica están en blanco por defecto — configure sus propias claves antes de materializar. Instale el paquete mediante FAR, configure las claves, materialice como tablas y comience a consultar.

## Primeros pasos

1. **Active el modo fhir-schema** — configure `fhir.validation.fhir-schema-validation=true` (obligatorio desde Aidbox 2604)
2. **Instale** `io.health-samurai.de-identification.r4` **mediante FAR**, o escriba sus propias ViewDefinitions
3. **Configure las claves criptográficas** en cada ViewDefinition — las claves en blanco serán rechazadas
4. **Materialice como tablas** — `$materialize` con `type=table` o mediante el constructor de ViewDefinition
5. **Consulte con SQL** desde la consola SQL de Aidbox, Grafana o cualquier herramienta de BI

Para la referencia completa de métodos y detalles de parámetros, consulte la [documentación de seudonimización](https://docs.aidbox.app/modules/sql-on-fhir/de-identification). Para probar el escenario completo de esta entrada con un único `docker compose up`, consulte el [ejemplo de seudonimización](https://github.com/Aidbox/examples/tree/main/aidbox-features/de-identification).