Privacy by Design is no longer just an ethical suggestion; it has become a mandatory legal standard under Paraguay’s Law 7593/2025. This legislative shift marks a milestone in the country’s digital maturity, forcing companies and developers to rethink how they capture, store, and protect citizen information. MITIC has been clear: generic terms and conditions won’t cut it; software must integrate technical protections from the initial architecture, not as an afterthought.
For the Paraguayan tech ecosystem, this represents an operational challenge but also a massive competitive advantage. Those who implement auditable consent flows and data minimization using modern tools will not only avoid million-dollar fines (which can exceed Gs. 2.2 billion) but also gain the trust of a market increasingly aware of its digital rights. If your application is still collecting data “just in case,” you already have a technical and legal debt that you must settle today. As I mentioned in my post on Technological Sovereignty, performance and privacy are user rights that we must defend through code.
What does Law 7593/2025 require from your software?#
The law states that the data controller must implement technical and organizational measures from the system’s design phase. It’s not a checklist added before deployment. It’s architecture.
The technical translation of the law is straightforward:
| Law Article | Description | Technical Requirement |
|---|---|---|
| Art. 4(d) — Minimization | Data “strictly limited to what is necessary” | Validate that only necessary and relevant fields are collected |
| Art. 6 — Consent | ”Prior, free, informed, and unambiguous” | Explicit opt-in (no pre-checked boxes) + audited record of legal version |
| Art. 9 — Responsibility | ”Appropriate technical and organizational measures” | Privacy-by-design + privacy-by-default in the architecture |
| Art. 16 — Security | ”Continuous monitoring and improvement” | Encryption in transit (TLS 1.3+) and at rest (AES-256) + periodic scans |
| Art. 17 — Incidents | Notification in ≤72 hours | Automatic detection + response playbooks + forensic logging |
| Arts. 26-31 — ARCO Rights | Access, rectification, erasure within ≤30 days | Export API + deletion endpoint + SLA timer |
If your contact form asks for “phone”, “address”, and “occupation” for a 3-field newsletter, Art. 4(d) is already flagging you.
How to implement Privacy by Design in your stack?#
The short answer: don’t store what you don’t need, encrypt what you do store, and let the user delete everything whenever they want. The technical answer has 4 steps.
Step 1: Data Minimization#
Every field you add to a form is a legal liability. Validate in the schema, not just the frontend:
import * as import zz from "zod";
// Schema that rejects unnecessary data by design
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),
// No phone, no full name, no address
// If the business needs more data, justify it explicitly
}); // z.strictObject() rejects undefined fields
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>;
Zod 4’s z.strictObject() is no minor detail: it rejects any field not in the schema. If someone tries to inject phone, address, or social_security_number into the POST request, Zod throws an error. Minimization becomes unbreakable.
Step 2: Explicit Consent#
A pre-checked box is not consent under Law 7593. You need an explicit, audited opt-in:
export interface ConsentRecord {
ConsentRecord.userId: stringuserId: string;
ConsentRecord.purpose: stringpurpose: string;
ConsentRecord.timestamp: Datetimestamp: Date;
ConsentRecord.userAgent: stringuserAgent: string;
// Hash of the legal text in effect at the time of consent
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,
};
} The legalTextVersion is key: if you update the terms and conditions, you need to know which version each user accepted. Without this, you cannot demonstrate valid consent.
Step 3: Encryption and Retention#
The data you store has an expiration date. Indefinite retention is automatic non-compliance:
// Helper that anonymizes emails for retention
function function anonymizeEmail(): stringanonymizeEmail(): string {
return `anon-${const crypto: {
randomUUID: () => string;
}
crypto.randomUUID: () => stringrandomUUID()}@anonymized.local`;
}
// Daily job that purges expired data (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);
// Delete expired consent records
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 } },
});
// Personal data without active purpose is also deleted
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: "Anonymized User",
// Soft delete → hard delete after 30 days
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(`Purge complete: ${const deleted: {
count: number;
}
deleted.count: numbercount ?? 0} records removed`);
} Anonymization is key: you don’t remove the user from the system (they might have associated orders), but you clean up personal data that no longer serves an active purpose.
Step 4: Right to Access and Deletion#
Art. 17 obliges responding to access, rectification, and erasure requests within ≤30 days. Automate it:
// 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 } },
// Only necessary data — no unnecessary joins
},
});
if (!const user: anyuser) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("User not found");
}
// Generate portable JSON (standard format)
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. Anonymize data before deleting
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}@anonymized.local`,
name: stringname: "Deleted by request",
},
});
// 2. Delete consents (no longer valid)
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. Audit log: who deleted, when, and why
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(),
},
});
} 🛠️Architectural AnalysisHaz clic para expandir
Privacy by Design is not a middleware added at the end. It’s an architectural principle that affects every layer: the schema validates what enters, the database records consents, jobs purge what’s expired, and APIs expose control to the user. If your stack lacks these 4 components, your “compliance” is merely cosmetic.
What is the cost of NOT complying with data laws in Paraguay?#
The question every CTO must ask before leaving “we’ll fix it later” in their backlog:
| Cost | Design-First Implementation | Post-Fine Fix |
|---|---|---|
| Development | 40-80 hours (once) | 120-200 hours (urgent) |
| Potential Fine | Gs. 0 | Up to Gs. 1.115 billion |
| Reputation Impact | Positive (differentiator) | Negative (notifiable) |
| Refactor Complexity | — | High (data already migrated) |
Compliance vs Developer Experience
Pros
- Less data to store = less to protect
- Strict schemas reduce validation bugs
- Audited consent legally protects the company
- Automation eliminates manual labor for ARCO requests
Cons
- Short forms might reduce initial leads
- Consent logging adds overhead to the database
- Purge jobs require monitoring and alerts
- Legal team needs to review every data purpose
Senior tip: less data collected = less to protect, less to store, less to respond to in an ARCO request. Minimization isn’t just compliance—it’s operational sanity.
Is your software compliant? A quick checklist#
- Minimization: Does every field in your form have a documented purpose? - [ ] Consent: Do you record who accepted what, when, and which version of the terms? - [ ] Encryption: TLS 1.3+ in transit? AES-256 at rest? - [ ] Retention: Do you have an automated job that purges expired data? - [ ] Export: Can users download all their data in a portable format? - [ ] Deletion: Can you delete/anonymize a user without breaking foreign keys or history? - [ ] Audit: Is every access to personal data recorded in a log? - [ ] DPO: Do you have an identifiable data officer (as required by law)?
Law 7593/2025 is not a bureaucratic hurdle. It’s an opportunity to stop hoarding pointless data and start building software that respects its users. Privacy by Design doesn’t make you less agile—it makes you more professional. In Paraguay, where the digital transformation of agriculture and trade is just taking off (as we saw in my publication calendar), being the one who complies first is a differentiator that both AIs and customers will notice.
FAQ on Law 7593/2025#
Does Law 7593/2025 apply to small businesses and freelance developers?
Yes. The law does not distinguish between company size or data volume. Every data controller—whether it's a cooperative in Itapúa, an Asunción startup, or a freelancer with a contact form—must implement Privacy by Design and appropriate technical measures.
Is it mandatory to appoint a DPO (Data Protection Officer)?
The law does not explicitly mandate a dedicated DPO, but it does require identifying a data controller. In practice, this means someone in your organization must be the point of contact for ARCO requests and MITIC. For small companies, the CTO or lead developer can fulfill this role.
How long do I have to respond to an access or deletion request?
Law 7593 establishes a maximum period of 30 calendar days to respond to requests for access, rectification, erasure, or opposition. Without an automated process (Export API + Deletion endpoint), meeting this deadline manually becomes unfeasible at scale.