La privacidad por diseño deja de ser una sugerencia ética para transformarse en el estándar legal obligatorio bajo la Ley 7593/2025 de Paraguay. Este cambio legislativo marca un hito en la madurez digital del país, obligando a empresas y desarrolladores a repensar cómo capturan, almacenan y protegen la información ciudadana. El MITIC ha sido claro: no bastará con términos y condiciones genéricos; el software debe integrar protecciones técnicas desde la arquitectura inicial, no como un parche posterior.
Para el ecosistema tech paraguayo, esto representa un desafío operativo pero también una ventaja competitiva enorme. Aquellos que implementen flujos de consentimiento auditables y minimización de datos mediante herramientas modernas, no solo evitarán multas millonarias (que pueden superar los Gs. 2.200 millones), sino que ganarán la confianza de un mercado cada vez más consciente de sus derechos digitales. Si tu aplicación todavía recolecta datos “por si acaso”, ya tenés una deuda técnica y legal que tenés que saldar hoy mismo. Como mencioné en mi post sobre Soberanía Tecnológica, el rendimiento y la privacidad son derechos del usuario que debemos defender desde el código.
¿Qué exige la Ley 7593/2025 a tu software?#
La ley establece que el responsable del tratamiento de datos debe implementar medidas técnicas y organizativas desde el diseño del sistema. No es un checklist que se añade antes del deploy. Es arquitectura.
La traducción técnica de la ley es directa:
| Artículo de Ley | Qué dice | Requerimiento Técnico |
|---|---|---|
| Art. 4(d) — Minimización | Datos “limitados estrictamente a lo necesario” | Validar que solo se recopilan campos necesarios y pertinentes |
| Art. 6 — Consentimiento | ”Previo, libre, informado e inequívoco” | Opt-in explícito (no casilla pre-marcada) + registro auditado de versión legal |
| Art. 9 — Responsable | ”Medidas técnicas y organizativas apropiadas” | Privacy-by-design + privacy-by-default en la arquitectura |
| Art. 16 — Seguridad | ”Monitoreo y mejora continua” | Cifrado en tránsito (TLS 1.3+) y en reposo (AES-256) + scans periódicos |
| Art. 17 — Incidentes | Notificación en ≤72 horas | Detección automática + playbooks de respuesta + logging forense |
| Arts. 26-31 — Derechos ARCO | Acceso, rectificación, supresión en ≤30 días | API de exportación + endpoint de eliminación + timer de SLA |
Si tu formulario de contacto pide “teléfono”, “dirección” y “ocupación” para un newsletter de 3 campos, el Art. 4(d) ya te está señalando.
¿Cómo implementar privacidad por diseño en tu stack?#
La respuesta corta: no guardes lo que no necesitás, cifrá lo que sí guardás, y dejá que el usuario borre todo cuando quiera. La respuesta técnica tiene 4 pasos.
Paso 1: Minimización de datos#
Cada campo que agregás a un formulario es una responsabilidad legal. Validá en el schema, no en el frontend:
import * as import zz from "zod";
// Schema que rechaza datos innecesarios por diseño
export const const ContactFormSchema: z.ZodObject<{
email: z.ZodEmail;
message: z.ZodString;
}, z.core.$strict>
ContactFormSchema = import zz.function strictObject<{
email: z.ZodEmail;
message: z.ZodString;
}>(shape: {
email: z.ZodEmail;
message: z.ZodString;
}, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
email: z.ZodEmail;
message: z.ZodString;
}, z.core.$strict>
strictObject({
email: z.ZodEmailemail: import zz.function email(params?: string | z.core.$ZodEmailParams): z.ZodEmailemail(),
message: z.ZodStringmessage: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()._ZodString<$ZodStringInternals<string>>.min(minLength: number, params?: string | z.core.$ZodCheckMinLengthParams): z.ZodStringmin(10)._ZodString<$ZodStringInternals<string>>.max(maxLength: number, params?: string | z.core.$ZodCheckMaxLengthParams): z.ZodStringmax(500),
// Sin teléfono, sin nombre completo, sin dirección
// Si el negocio necesita más datos, justificarlo explícitamente
}); // z.strictObject() rechaza campos no definidos
export type type ContactForm = {
email: string;
message: string;
}
ContactForm = import zz.type infer<T> = T extends {
_zod: {
output: any;
};
} ? T["_zod"]["output"] : unknown
export infer
infer<typeof const ContactFormSchema: z.ZodObject<{
email: z.ZodEmail;
message: z.ZodString;
}, z.core.$strict>
ContactFormSchema>;
z.strictObject() de Zod 4 no es un detalle menor: rechaza cualquier campo que no esté en el schema. Si alguien intenta inyectar phone, address o social_security_number en el POST, Zod tira un error. La minimización se vuelve inquebrantable.
Paso 2: Consentimiento explícito#
Una casilla pre-marcada no es consentimiento bajo la Ley 7593. Necesitás un opt-in explícito y auditado:
export interface ConsentRecord {
ConsentRecord.userId: stringuserId: string;
ConsentRecord.purpose: stringpurpose: string;
ConsentRecord.timestamp: Datetimestamp: Date;
ConsentRecord.userAgent: stringuserAgent: string;
// El hash del texto legal vigente al momento del consentimiento
ConsentRecord.legalTextVersion: stringlegalTextVersion: string;
}
export async function function recordConsent(userId: string, purpose: string, request: Request): Promise<ConsentRecord>recordConsent(
userId: stringuserId: string,
purpose: stringpurpose: string,
request: Requestrequest: Request,
): interface Promise<T>Represents the completion of an asynchronous operationPromise<ConsentRecord> {
const const result: anyresult = await const db: {
consent: {
create: (args: any) => Promise<any>;
};
}
db.consent: {
create: (args: any) => Promise<any>;
}
consent.create: (args: any) => Promise<any>create({
data: {
userId: string;
purpose: string;
timestamp: Date;
userAgent: string;
legalTextVersion: string;
}
data: {
userId: stringuserId,
purpose: stringpurpose,
timestamp: Datetimestamp: new var Date: DateConstructor
new () => Date (+4 overloads)
Date(),
userAgent: stringuserAgent: request: Requestrequest.Request.headers: {
get(name: string): string | null;
}
headers.function get(name: string): string | nullget("user-agent") ?? "unknown",
legalTextVersion: stringlegalTextVersion: function getCurrentLegalVersion(): stringgetCurrentLegalVersion(),
},
});
return {
ConsentRecord.userId: stringuserId: const result: anyresult.userId,
ConsentRecord.purpose: stringpurpose: const result: anyresult.purpose,
ConsentRecord.timestamp: Datetimestamp: const result: anyresult.timestamp,
ConsentRecord.userAgent: stringuserAgent: const result: anyresult.userAgent,
ConsentRecord.legalTextVersion: stringlegalTextVersion: const result: anyresult.legalTextVersion,
};
} El legalTextVersion es clave: si actualizás los términos y condiciones, necesitás saber qué versión aceptó cada usuario. Sin esto, no podés demostrar el consentimiento.
Paso 3: Cifrado y retención#
Los datos que guardás tienen fecha de vencimiento. La retención indefinida es incumplimiento automático:
// Helper que anonimiza emails para retención
function function anonymizeEmail(): stringanonymizeEmail(): string {
return `anon-${const crypto: {
randomUUID: () => string;
}
crypto.randomUUID: () => stringrandomUUID()}@anonimizado.local`;
}
// Job diario que purga datos expirados (cron: 0 2 * * *)
export async function function purgeExpiredData(): Promise<void>purgeExpiredData() {
const const RETENTION_DAYS: 365RETENTION_DAYS = 365;
const const cutoff: Datecutoff = new var Date: DateConstructor
new () => Date (+4 overloads)
Date();
const cutoff: Datecutoff.Date.setDate(date: number): numberSets the numeric day-of-the-month value of the Date object using local time.setDate(const cutoff: Datecutoff.Date.getDate(): numberGets the day-of-the-month, using local time.getDate() - const RETENTION_DAYS: 365RETENTION_DAYS);
// Eliminar registros de consentimiento expirados
const const deleted: {
count: number;
}
deleted = await const db: {
consent: {
deleteMany: (args: any) => Promise<{
count: number;
}>;
};
user: {
updateMany: (args: any) => Promise<any>;
};
}
db.consent: {
deleteMany: (args: any) => Promise<{
count: number;
}>;
}
consent.deleteMany: (args: any) => Promise<{
count: number;
}>
deleteMany({
where: {
updatedAt: {
lt: Date;
};
}
where: { updatedAt: {
lt: Date;
}
updatedAt: { lt: Datelt: const cutoff: Datecutoff } },
});
// Los datos personales sin propósito activa también se eliminan
await const db: {
consent: {
deleteMany: (args: any) => Promise<{
count: number;
}>;
};
user: {
updateMany: (args: any) => Promise<any>;
};
}
db.user: {
updateMany: (args: any) => Promise<any>;
}
user.updateMany: (args: any) => Promise<any>updateMany({
where: {
AND: ({
lastActiveAt: {
lt: Date;
};
} | {
purposeConsent: null;
})[];
}
where: {
type AND: ({
lastActiveAt: {
lt: Date;
};
} | {
purposeConsent: null;
})[]
AND: [{ lastActiveAt: {
lt: Date;
}
lastActiveAt: { lt: Datelt: const cutoff: Datecutoff } }, { purposeConsent: nullpurposeConsent: null }],
},
data: {
email: string;
name: string;
status: string;
}
data: {
email: stringemail: function anonymizeEmail(): stringanonymizeEmail(),
name: stringname: "Usuario Anonimizado",
// Soft delete → hard delete tras 30 días
status: stringstatus: "pending_deletion",
},
});
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(`Purga completada: ${const deleted: {
count: number;
}
deleted.count: numbercount ?? 0} registros eliminados`);
} La anonimización es clave: no eliminás al usuario del sistema (puede que tenga órdenes asociadas), pero limpiás los datos personales que ya no tienen propósito activo.
Paso 4: Derecho de acceso y eliminación#
El Art. 17 obliga a responder solicitudes de acceso, rectificación y eliminación en ≤30 días. Automatizalo:
// POST /api/user/:id/export
export async function function exportUserData(userId: string): Promise<{
personalData: {
email: any;
name: any;
createdAt: any;
};
consents: any;
orders: any;
}>
exportUserData(userId: stringuserId: string) {
const const user: anyuser = await const db: {
user: {
findUnique: (args: any) => Promise<any>;
update: (args: any) => Promise<any>;
};
consent: {
deleteMany: (args: any) => Promise<any>;
};
auditLog: {
create: (args: any) => Promise<any>;
};
}
db.user: {
findUnique: (args: any) => Promise<any>;
update: (args: any) => Promise<any>;
}
user.findUnique: (args: any) => Promise<any>findUnique({
where: {
id: string;
}
where: { id: stringid: userId: stringuserId },
include: {
consents: boolean;
orders: {
select: {
id: boolean;
date: boolean;
total: boolean;
};
};
}
include: {
consents: booleanconsents: true,
orders: {
select: {
id: boolean;
date: boolean;
total: boolean;
};
}
orders: { select: {
id: boolean;
date: boolean;
total: boolean;
}
select: { id: booleanid: true, date: booleandate: true, total: booleantotal: true } },
// Solo datos necesarios — no joins innecesarios
},
});
if (!const user: anyuser) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("Usuario no encontrado");
}
// Generar JSON portátil (formato estándar)
return {
personalData: {
email: any;
name: any;
createdAt: any;
}
personalData: {
email: anyemail: const user: anyuser.email ?? "",
name: anyname: const user: anyuser.name ?? "",
createdAt: anycreatedAt: const user: anyuser.createdAt,
},
consents: anyconsents: const user: anyuser.consents,
orders: anyorders: const user: anyuser.orders,
};
}
// DELETE /api/user/:id
export async function function deleteUserData(userId: string): Promise<void>deleteUserData(userId: stringuserId: string) {
// 1. Anonimizar datos antes de eliminar
await const db: {
user: {
findUnique: (args: any) => Promise<any>;
update: (args: any) => Promise<any>;
};
consent: {
deleteMany: (args: any) => Promise<any>;
};
auditLog: {
create: (args: any) => Promise<any>;
};
}
db.user: {
findUnique: (args: any) => Promise<any>;
update: (args: any) => Promise<any>;
}
user.update: (args: any) => Promise<any>update({
where: {
id: string;
}
where: { id: stringid: userId: stringuserId },
data: {
email: string;
name: string;
}
data: {
email: stringemail: `deleted-${userId: stringuserId}@anonimizado.local`,
name: stringname: "Eliminado por solicitud",
},
});
// 2. Eliminar consentimientos (ya no son válidos)
await const db: {
user: {
findUnique: (args: any) => Promise<any>;
update: (args: any) => Promise<any>;
};
consent: {
deleteMany: (args: any) => Promise<any>;
};
auditLog: {
create: (args: any) => Promise<any>;
};
}
db.consent: {
deleteMany: (args: any) => Promise<any>;
}
consent.deleteMany: (args: any) => Promise<any>deleteMany({ where: {
userId: string;
}
where: { userId: stringuserId } });
// 3. Log de auditoría: quién eliminó, cuándo y por qué
await const db: {
user: {
findUnique: (args: any) => Promise<any>;
update: (args: any) => Promise<any>;
};
consent: {
deleteMany: (args: any) => Promise<any>;
};
auditLog: {
create: (args: any) => Promise<any>;
};
}
db.auditLog: {
create: (args: any) => Promise<any>;
}
auditLog.create: (args: any) => Promise<any>create({
data: {
action: "USER_DELETED";
userId: string;
timestamp: Date;
}
data: {
action: "USER_DELETED"action: "USER_DELETED" as type const = "USER_DELETED"const,
userId: stringuserId,
timestamp: Datetimestamp: new var Date: DateConstructor
new () => Date (+4 overloads)
Date(),
},
});
} 🛠️Análisis de ArquitecturaHaz clic para expandir
La privacidad por diseño no es un middleware que se añade al final. Es un principio de arquitectura que afecta cada capa: el schema valida qué entra, la base de datos registra consentimientos, los jobs purgan lo expirado, y las APIs exponen control al usuario. Si tu stack no tiene estos 4 componentes, tu “compliance” es cosmético.
¿Cuánto cuesta NO cumplir la ley de datos en Paraguay?#
La pregunta que todo CTO tiene que hacerse antes de dejar el “después lo arreglamos” en su backlog:
| Costo | Implementar desde diseño | Arreglar post-multas |
|---|---|---|
| Desarrollo | 40-80 horas (una vez) | 120-200 horas (urgente) |
| Multa potencial | Gs. 0 | Hasta Gs. 1.115 millones |
| Impacto en reputación | Positivo (diferencial) | Negativo (notificable) |
| Complejidad del refactor | — | Alta (datos ya migrados) |
Compliance vs Developer Experience
Pros
- Menos datos que almacenar = menos que proteger
- Schemas estrictos reducen bugs de validación
- El consentimiento auditado protege a la empresa legalmente
- La automatización elimina el trabajo manual de solicitudes ARCO
Cons
- Los formularios cortos pueden reducir leads iniciales
- El registro de consentimiento agrega overhead a la base de datos
- Los jobs de purga requieren monitoreo y alertas
- El equipo legal necesita revisar cada propósito de datos
Consejo senior: menos datos recolectados = menos que proteger, menos que almacenar, menos que responder en un ARCO. La minimización no es solo compliance — es sanidad operativa.
¿Está tu software cumpliendo? Checklist rápido#
- Minimización: ¿Cada campo de tu formulario tiene un propósito documentado? - [ ] Consentimiento: ¿Registrás quién aceptó qué, cuándo y qué versión de los términos? - [ ] Cifrado: ¿TLS 1.3+ en tránsito? ¿AES-256 en reposo? - [ ] Retención: ¿Tenés un job automático que purga datos expirados? - [ ] Exportación: ¿El usuario puede descargar todos sus datos en formato portátil? - [ ] Eliminación: ¿Podés borrar/anonimizar un usuario sin romper FK o historial? - [ ] Auditoría: ¿Cada acceso a datos personales queda registrado en un log? - [ ] DPO: ¿Tenés un responsable de datos identificable (como exige la ley)?
La Ley 7593/2025 no es un obstáculo burocrático. Es una oportunidad para dejar de hoardrear datos sin propósito y construir software que respete a sus usuarios. La privacidad por diseño no te hace menos ágil — te hace más profesional. Y en Paraguay, donde la transformación digital del agro y el comercio apenas arranca (como vimos en mi calendario de publicaciones), ser el que cumple primero es un diferencial que las IAs y los clientes van a notar.
Preguntas Frecuentes sobre la Ley 7593/2025#
¿La Ley 7593/2025 aplica a empresas pequeñas y desarrolladores freelance?
Sí. La ley no distingue por tamaño de empresa ni volumen de datos. Todo responsable del tratamiento de datos personales — sea una cooperativa de Itapúa, una startup de Asunción o un freelance con un formulario de contacto — debe implementar privacidad por diseño y medidas técnicas adecuadas.
¿Necesito nombrar un DPO (Data Protection Officer) obligatorio?
La ley no exige explícitamente un DPO dedicado, pero sí requiere identificar un responsable del tratamiento de datos. En la práctica, esto significa que alguien en tu organización debe ser el punto de contacto para solicitudes ARCO y ante el MITIC. Para empresas pequeñas, el mismo CTO o desarrollador principal puede cumplir este rol.
¿Cuánto tiempo tengo para responder una solicitud de acceso o eliminación?
La Ley 7593 establece un plazo máximo de 30 días calendario para responder solicitudes de acceso, rectificación, supresión u oposición. Si no tenés un proceso automatizado (API de exportación + endpoint de eliminación), cumplir este plazo manualmente se vuelve inviable a escala.