Portabilidad de datos RGPD en una web a medida
Cómo implementar la portabilidad y exportación de datos (RGPD) en una aplicación web a medida
Hay un derecho del RGPD que se suele dejar para el final y que, cuando llega la primera solicitud real de un usuario, pilla a medio equipo de desarrollo improvisando un SELECT * a las nueve de la noche. Hablo del derecho de portabilidad. No es un capricho legal ni una casilla más en la política de privacidad: es una funcionalidad que tu aplicación debería ser capaz de ejecutar casi sin intervención humana. Y conviene diseñarla bien desde el principio, porque hacerla a posteriori sobre una base de datos que ha crecido a su aire es bastante más doloroso.
En este artículo vamos a ver, con criterio de ingeniería y sin perder de vista el marco de la AEPD y la LOPDGDD, cómo se monta de verdad la portabilidad y la exportación de datos en una web o app a medida. Qué obliga la ley, qué formato sirve y cuál no, cómo evitar tumbar la aplicación al generar el fichero y dónde están las trampas de seguridad que casi nadie tiene en cuenta.
Qué dice realmente el artículo 20 del RGPD
El derecho de portabilidad está recogido en el artículo 20 del RGPD y dice, resumido, que el interesado tiene derecho a recibir los datos personales que haya facilitado a un responsable del tratamiento en un formato estructurado, de uso común y lectura mecánica, y a transmitirlos a otro responsable sin que el primero se lo impida.
La gente lo confunde a menudo con el derecho de acceso (art. 15), pero no son lo mismo, y la diferencia tiene consecuencias técnicas directas:
- El derecho de acceso te obliga a contarle al usuario qué datos suyos tratas, con qué fines, durante cuánto tiempo, a quién se los cedes... Es informativo. Puede entregarse incluso en un texto legible por una persona.
- El derecho de portabilidad va más allá: tienes que devolverle sus datos en un formato que otra máquina pueda volver a importar. No es para que el usuario lo lea, es para que se lo lleve a otro sitio.
Y ojo, porque la portabilidad no aplica a todo. El artículo 20 acota bastante el alcance, y esto te ahorra trabajo si lo entiendes bien. Solo aplica a datos que cumplan las tres condiciones a la vez:
- Son datos facilitados por el propio usuario (los que ha introducido él, o los que se han generado por su actividad, como su historial de pedidos; no los que tú has inferido o calculado por tu cuenta).
- El tratamiento se basa en el consentimiento o en la ejecución de un contrato. Si tratas esos datos por una obligación legal o por interés legítimo, la portabilidad no entra.
- El tratamiento se realiza por medios automatizados. Lo que esté en papel en un archivador queda fuera.
Esto último es importante a la hora de filtrar. No tienes que exportar absolutamente todo lo que hay en la base de datos asociado a un usuario. Tienes que exportar lo que él aportó y que tratas por consentimiento o contrato de forma automatizada. Los datos derivados (un scoring interno, una segmentación que tú has calculado, perfiles construidos por tu algoritmo) normalmente quedan fuera de la portabilidad, aunque puedan estar sujetos al derecho de acceso.
El formato: por qué un PDF no vale
Esta es la parte donde más implementaciones suspenden. "De uso común y lectura mecánica" no es una frase decorativa. Significa que el fichero tiene que poder ser parseado por un programa sin intervención manual. En la práctica:
- JSON, CSV y XML cumplen. Son formatos abiertos, documentados y que cualquier sistema sabe leer.
- Un PDF no cumple para la portabilidad. Un PDF está pensado para que lo lea una persona, no una máquina. Extraer datos estructurados de un PDF es un infierno y, formalmente, no encaja con lo que pide el artículo 20.
- Un pantallazo, un documento de Word o un email con el texto pegado tampoco valen.
Mi recomendación práctica: JSON como formato principal, porque maneja bien estructuras anidadas (un usuario con sus pedidos, y cada pedido con sus líneas), y CSV adicional para quien quiera abrirlo en una hoja de cálculo. Un JSON limpio de la exportación se parece a esto:
{
"exportacion": {
"generada": "2026-06-09T18:42:00+02:00",
"version_formato": "1.0",
"usuario": {
"id": "u_84213",
"email": "persona@ejemplo.es",
"nombre": "Lucía",
"alta": "2023-11-04"
},
"pedidos": [
{ "id": "p_1001", "fecha": "2024-02-12", "total": 49.90, "estado": "entregado" }
],
"preferencias": { "newsletter": true, "idioma": "es" }
}
}
Fíjate en un detalle: incluyo version_formato. Si dentro de dos años cambias la estructura, quien importe el fichero sabrá qué espera encontrar. Es el tipo de cosa que distingue una implementación pensada de un volcado improvisado.
Diseño técnico: el panel de "descargar mis datos"
Vamos al cómo. La forma más sensata de resolver esto en una aplicación a medida es un endpoint o un panel de autoservicio donde el usuario, una vez autenticado, pulsa "Descargar mis datos" y el sistema se encarga del resto. Cuanto menos dependa de que una persona del equipo procese la solicitud a mano, menos riesgo de incumplir plazos y menos coste operativo.
Recolectar los datos repartidos por todo el sistema
El primer reto real es que los datos de un usuario no están en una sola tabla. Están en usuarios, en pedidos, en direcciones, en mensajes, en los logs de un microservicio de notificaciones, quizá en un CRM externo... Necesitas un recolector que sepa recorrer todas esas fuentes y juntar lo que pertenece a esa persona.
La consulta base no tiene misterio, pero la clave es que esté acotada al usuario y filtrada por lo que de verdad es portable:
SELECT p.id, p.fecha, p.total, p.estado
FROM pedidos p
WHERE p.usuario_id = :uid
AND p.origen_dato = 'usuario';
Lo que diferencia un buen diseño es centralizar esta lógica. En lugar de tener cien consultas dispersas, defines en cada módulo o servicio un "proveedor de exportación" que sabe qué datos suyos son portables y los devuelve normalizados. Así, cuando mañana añadas una tabla nueva, sabes exactamente dónde tienes que registrar su contribución a la exportación. Si no lo haces así, cada vez que crezca el modelo de datos te arriesgas a olvidarte de una fuente, y una exportación incompleta también es un incumplimiento.
Hazlo asíncrono o tumbarás la app
Aquí va el error de rendimiento más típico. Si el usuario pulsa el botón y tu backend se pone a recorrer quince tablas, comprimir, generar el fichero y todo eso dentro del mismo ciclo de petición HTTP, vas a tener timeouts, peticiones colgadas y, con varios usuarios a la vez, una caída del servicio. Y si alguien tiene diez años de historial, ni te cuento.
La exportación tiene que ser asíncrona. El patrón es bien conocido:
- El usuario solicita la exportación. El servidor responde inmediatamente con un "estamos preparando tus datos, te avisaremos".
- Se encola un job en un sistema de colas (Redis, RabbitMQ, SQS, o lo que uses).
- Un worker en segundo plano recoge el job, recopila los datos de todas las fuentes, genera el fichero y lo guarda en un almacenamiento seguro.
- Cuando termina, se notifica al usuario (por email o dentro de la app) con un enlace de descarga.
Un esqueleto del worker en Python, para que se vea la idea:
def procesar_exportacion(solicitud_id):
solicitud = repo.get(solicitud_id)
datos = recolectar_datos_usuario(solicitud.usuario_id) # recorre todas las fuentes
fichero = generar_json(datos)
url = almacenar_temporal(fichero, expira_en_horas=48)
repo.marcar_completada(solicitud_id, url)
notificar_usuario(solicitud.usuario_id, url)
Este desacoplamiento es lo que permite que una exportación pesada no afecte a la experiencia del resto de usuarios. Es también, dicho sea de paso, una de esas decisiones de arquitectura donde merece la pena tener a alguien con experiencia en el equipo, porque equivocarse aquí se paga en producción.
Seguridad: la parte que más se descuida
Estás a punto de entregar todos los datos personales de alguien en un único fichero. Si la implementación tiene un fallo, has convertido una funcionalidad de cumplimiento en una brecha de seguridad. Estos son los puntos que no puedes saltarte:
- Verifica la identidad antes de exportar. No basta con que la petición venga firmada. Una solicitud de portabilidad debería exigir reautenticación: pedir de nuevo la contraseña, un segundo factor o un código enviado a un canal de confianza. La pesadilla es exportar todos los datos de un usuario porque alguien dejó la sesión abierta en un ordenador compartido.
- Enlace de descarga temporal y firmado. El fichero no se sirve desde una URL pública adivinable. Se genera una URL firmada con expiración, por ejemplo válida 24 o 48 horas, y que caduca después. Pasado ese tiempo, el enlace deja de funcionar y el fichero se borra.
- Cifra el fichero, tanto en tránsito (HTTPS, evidente) como en reposo en el almacenamiento donde lo dejas mientras el usuario lo descarga.
- No incluyas datos de terceros. Esto es delicado. Si en los mensajes de ese usuario aparecen datos de otra persona, o si un pedido compartido implica a un tercero, no puedes volcarlos sin más. El propio artículo 20 dice que la portabilidad no debe menoscabar los derechos de otros. Filtra o anonimiza lo que corresponda a otros interesados.
Plazos, gratuidad y registro
El marco legal impone unas condiciones que tu sistema debe respetar, y conviene que estén automatizadas:
- Plazo de un mes desde la recepción de la solicitud para resolverla. Es prorrogable otros dos meses si la solicitud es compleja o tienes muchas, pero hay que comunicar la prórroga al interesado dentro del primer mes. Un panel de autoservicio bien hecho resuelve esto en minutos, no en semanas.
- Gratuidad. El ejercicio del derecho es gratuito. Solo en casos manifiestamente infundados o excesivos (por ejemplo, repetitivos) podrías cobrar un canon razonable o negarte, y tendrías que justificarlo.
- Registro de la solicitud. Guarda traza de cada petición: quién la pidió, cuándo, cómo se verificó la identidad, cuándo se entregó y qué se incluyó. Esto es lo que te permite acreditar el cumplimiento ante la AEPD si algún día te lo reclaman. El principio de responsabilidad proactiva del RGPD funciona así: no basta con cumplir, hay que poder demostrarlo.
Portabilidad directa entre responsables
Hay un matiz del artículo 20 que casi nadie implementa pero conviene conocer: cuando es técnicamente posible, el usuario tiene derecho a que sus datos se transmitan directamente de un responsable a otro, sin pasar por él. En la práctica esto significa exponer o consumir una API de importación/exportación entre plataformas. La ley no te obliga a inventar interoperabilidad universal, pero sí a no poner trabas si la transmisión directa es factible. Si tu sector tiene estándares de intercambio, tenerlos en cuenta en el diseño te ahorra problemas.
Los errores más comunes (y cómo evitarlos)
Para cerrar, un repaso a los fallos que más veo en auditorías y revisiones de código:
- Volcar el dump completo de la base de datos sin filtrar. Es el clásico. Acabas exportando datos derivados que no son portables, datos de terceros y campos internos que no deberían salir. Filtra por lo que de verdad facilitó el usuario.
- Entregar formatos no legibles por máquina. El PDF, el pantallazo, el Word. No cumplen. JSON, CSV o XML.
- No autenticar la petición. Exportar sin reverificar identidad es abrir la puerta a que cualquiera con una sesión secuestrada se lleve los datos.
- Hacerlo síncrono y descubrir el problema el día que un usuario con mucho historial tira la aplicación.
- No registrar nada, y quedarte sin forma de demostrar que cumpliste el plazo cuando llegue la reclamación.
La portabilidad bien implementada no es un coste muerto: es una pieza de confianza con tus usuarios y una de esas cosas que, hechas a tiempo, te evitan sustos. Si estás diseñando una aplicación a medida y quieres que el cumplimiento del RGPD esté integrado en la arquitectura desde el primer día en lugar de parcheado al final, en Tangram Consulting podemos ayudarte: cuéntanos tu proyecto y lo revisamos contigo.
Implementar el artículo 20 no es difícil cuando lo enfocas como lo que es, una funcionalidad más del producto, con su endpoint, su cola, su seguridad y su trazabilidad. Lo difícil es acordarse de ello cuando todavía estás a tiempo de diseñarlo bien.