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 LeyQué diceRequerimiento Técnico
Art. 4(d) — MinimizaciónDatos “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 — IncidentesNotificación en ≤72 horasDetección automática + playbooks de respuesta + logging forense
Arts. 26-31 — Derechos ARCOAcceso, rectificación, supresión en ≤30 díasAPI 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:

contact-form-schema.ts
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:

consent-tracker.ts
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 operation
Promise
<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:

data-retention-job.ts
// 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): number
Sets the numeric day-of-the-month value of the Date object using local time.
@paramdate A numeric value equal to the day of the month.
setDate
(const cutoff: Datecutoff.Date.getDate(): number
Gets 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[]): void
The **`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:

data-export-route.ts
// 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:

CostoImplementar desde diseñoArreglar post-multas
Desarrollo40-80 horas (una vez)120-200 horas (urgente)
Multa potencialGs. 0Hasta Gs. 1.115 millones
Impacto en reputaciónPositivo (diferencial)Negativo (notificable)
Complejidad del refactorAlta (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)?
Conclusión

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.

Hugo Campañoli
Escrito por

Hugo Campañoli

Arquitecto de Software & Especialista en Rendimiento Web. Construyo ecosistemas digitales de alta velocidad que dominan los buscadores y deleitan a los usuarios. Liderando la ingeniería de contenido desde Itapúa.