SaaS GDPR Engineering: Data Deletion, Consent Management, and Right-to-Erasure Pipelines
Build GDPR-compliant SaaS infrastructure: implement right-to-erasure pipelines that delete user data across all systems, manage consent with audit trails, handle data subject access requests (DSARs), and anonymize analytics data.
GDPR compliance for SaaS is mostly an engineering problem. The legal requirements (right to erasure, data portability, consent records) translate directly into system design decisions: how you store user data, whether you cascade deletes or anonymize, how you sync deletions to third-party systems, and whether your analytics pipeline retains PII.
The biggest mistake: treating GDPR as an afterthought and then trying to bolt deletion pipelines onto a schema that wasn't designed for them.
Data Inventory First
Before writing code, map every place user data lives:
Primary Database (PostgreSQL):
- users table (email, name, IP, preferences)
- audit_logs table (user actions ā may need to retain)
- analytics events (anonymize, don't delete)
- content created by user (may need to retain for other users)
Object Storage (S3):
- User avatar images
- Uploaded files
- Export files
Cache (Redis):
- Session data
- User preferences cache
Analytics (Segment, Mixpanel, Amplitude):
- Clickstream events with user_id
- Identity profiles
Email (Resend, SendGrid):
- Email addresses in contact lists
- Email delivery history
CRM (HubSpot, Salesforce):
- Contact records
- Deal history
Logs (CloudWatch, Datadog):
- May contain PII (IP addresses, emails in error messages)
- Typically 30ā90 day retention policy handles this
Right-to-Erasure Pipeline
// src/services/gdpr/erasure.service.ts
export type ErasureStrategy = "delete" | "anonymize" | "retain";
interface ErasureStep {
name: string;
strategy: ErasureStrategy;
execute: (userId: string) => Promise<void>;
}
export class ErasureService {
private readonly steps: ErasureStep[] = [
// Step 1: Anonymize analytics (can't delete ā breaks aggregate metrics)
{
name: "anonymize-analytics",
strategy: "anonymize",
execute: async (userId) => {
await db.query(
`UPDATE events
SET user_id = NULL,
anonymous_id = $2, -- Replace with stable but non-identifying hash
properties = properties - 'email' - 'name' - 'phone'
WHERE user_id = $1`,
[userId, `anon-${hashUserId(userId)}`]
);
},
},
// Step 2: Delete user-generated content (or anonymize if other users depend on it)
{
name: "handle-content",
strategy: "anonymize",
execute: async (userId) => {
// Comments: anonymize (other users see "[Deleted User]")
await db.query(
`UPDATE comments
SET user_id = NULL,
author_name = '[Deleted User]',
author_email = NULL
WHERE user_id = $1`,
[userId]
);
// Private files: delete
const { rows: files } = await db.query<{ s3_key: string }>(
"SELECT s3_key FROM user_files WHERE user_id = $1 AND visibility = 'private'",
[userId]
);
for (const file of files) {
await s3.deleteObject({
Bucket: process.env.S3_BUCKET!,
Key: file.s3_key,
});
}
await db.query(
"DELETE FROM user_files WHERE user_id = $1 AND visibility = 'private'",
[userId]
);
},
},
// Step 3: Delete S3 avatar
{
name: "delete-avatar",
strategy: "delete",
execute: async (userId) => {
await s3.deleteObject({
Bucket: process.env.S3_BUCKET!,
Key: `avatars/${userId}.webp`,
});
},
},
// Step 4: Revoke all sessions
{
name: "revoke-sessions",
strategy: "delete",
execute: async (userId) => {
await redis.del(`session:${userId}:*`); // Delete all session keys
await db.query(
"DELETE FROM refresh_tokens WHERE user_id = $1",
[userId]
);
},
},
// Step 5: Notify third-party services
{
name: "notify-third-parties",
strategy: "delete",
execute: async (userId) => {
const { rows } = await db.query<{ email: string }>(
"SELECT email FROM users WHERE id = $1",
[userId]
);
if (!rows[0]) return;
const email = rows[0].email;
// Delete from email service
await Promise.allSettled([
resend.contacts.remove({ email, audienceId: process.env.RESEND_AUDIENCE_ID! }),
hubspot.crm.contacts.basicApi.archive(email),
segment.identify({ userId, traits: { $delete: true } }),
]);
},
},
// Step 6: Anonymize audit logs (retain for legal compliance, remove PII)
{
name: "anonymize-audit-logs",
strategy: "anonymize",
execute: async (userId) => {
await db.query(
`UPDATE audit_log_entries
SET user_id = NULL,
user_email = NULL,
ip_address = NULL,
user_agent = NULL
WHERE user_id = $1`,
[userId]
);
},
},
// Step 7: Delete the user record (last ā all FK references cleaned first)
{
name: "delete-user-record",
strategy: "delete",
execute: async (userId) => {
await db.query("DELETE FROM users WHERE id = $1", [userId]);
},
},
];
async executeErasure(userId: string): Promise<ErasureResult> {
// Log the erasure request for compliance
const erasureId = await this.createErasureRecord(userId);
const results: { step: string; success: boolean; error?: string }[] = [];
for (const step of this.steps) {
try {
await step.execute(userId);
results.push({ step: step.name, success: true });
} catch (error) {
// Log failure but continue ā partial erasure is better than none
const message = error instanceof Error ? error.message : String(error);
results.push({ step: step.name, success: false, error: message });
console.error(`Erasure step ${step.name} failed for user ${userId}:`, error);
}
}
await this.completeErasureRecord(erasureId, results);
const allSucceeded = results.every((r) => r.success);
if (!allSucceeded) {
// Alert engineering team ā incomplete erasure needs manual review
await alertErasureFailure(userId, erasureId, results);
}
return { erasureId, completed: allSucceeded, steps: results };
}
private async createErasureRecord(userId: string): Promise<string> {
const { rows } = await db.query<{ id: string }>(
`INSERT INTO gdpr_erasure_requests
(user_id, requested_at, status)
VALUES ($1, NOW(), 'in_progress')
RETURNING id`,
[userId]
);
return rows[0].id;
}
}
š SaaS MVP in 8 Weeks ā Seriously
We have launched 50+ SaaS platforms. Multi-tenant architecture, Stripe billing, auth, role-based access, and cloud deployment ā all handled by one senior team.
- Week 1ā2: Architecture design + wireframes
- Week 3ā6: Core features built + tested
- Week 7ā8: Launch-ready on AWS/Vercel with CI/CD
- Post-launch: Maintenance plans from month 3
Consent Management
-- Consent events ā append-only ledger (never update, only insert)
CREATE TABLE consent_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
consent_type TEXT NOT NULL,
-- Types: 'marketing_email' | 'analytics' | 'functional' | 'personalization'
action TEXT NOT NULL, -- 'granted' | 'revoked'
source TEXT NOT NULL, -- 'signup_form' | 'settings_page' | 'api'
ip_address INET,
user_agent TEXT,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Store the consent text shown to the user at time of consent
consent_version TEXT NOT NULL,
consent_text TEXT NOT NULL
);
-- Current consent state: last action per user per type
CREATE VIEW current_consent AS
SELECT DISTINCT ON (user_id, consent_type)
user_id,
consent_type,
action AS current_state,
recorded_at AS last_updated,
consent_version
FROM consent_events
ORDER BY user_id, consent_type, recorded_at DESC;
// src/services/gdpr/consent.service.ts
export type ConsentType = "marketing_email" | "analytics" | "functional" | "personalization";
export type ConsentAction = "granted" | "revoked";
export async function recordConsent(params: {
userId: string;
type: ConsentType;
action: ConsentAction;
source: string;
ipAddress: string;
userAgent: string;
consentVersion: string; // Version of your Privacy Policy / cookie banner
consentText: string; // Exact text shown to the user
}): Promise<void> {
await db.query(
`INSERT INTO consent_events
(user_id, consent_type, action, source, ip_address, user_agent,
consent_version, consent_text)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
params.userId, params.type, params.action, params.source,
params.ipAddress, params.userAgent,
params.consentVersion, params.consentText,
]
);
// If analytics consent revoked: anonymize historical data
if (params.type === "analytics" && params.action === "revoked") {
await anonymizeUserAnalytics(params.userId);
}
}
export async function getUserConsents(userId: string): Promise<
Record<ConsentType, { granted: boolean; lastUpdated: Date }>
> {
const { rows } = await db.query(
"SELECT * FROM current_consent WHERE user_id = $1",
[userId]
);
return Object.fromEntries(
rows.map((row) => [
row.consent_type,
{ granted: row.current_state === "granted", lastUpdated: row.last_updated },
])
) as Record<ConsentType, { granted: boolean; lastUpdated: Date }>;
}
Data Subject Access Request (DSAR)
// src/services/gdpr/dsar.service.ts
// Generate a complete data export for a user
export async function generateDataExport(userId: string): Promise<string> {
const [user, events, files, consents, subscriptions] = await Promise.all([
db.query("SELECT * FROM users WHERE id = $1", [userId]),
db.query(
"SELECT event_type, occurred_at, properties FROM events WHERE user_id = $1 ORDER BY occurred_at DESC LIMIT 10000",
[userId]
),
db.query(
"SELECT filename, created_at, file_size FROM user_files WHERE user_id = $1",
[userId]
),
db.query(
"SELECT consent_type, action, recorded_at, consent_version FROM consent_events WHERE user_id = $1 ORDER BY recorded_at DESC",
[userId]
),
db.query(
"SELECT plan, status, created_at FROM subscriptions WHERE user_id = $1",
[userId]
),
]);
const exportData = {
exportedAt: new Date().toISOString(),
exportVersion: "1.0",
userData: {
profile: user.rows[0],
activityHistory: events.rows,
files: files.rows,
consentHistory: consents.rows,
subscriptions: subscriptions.rows,
},
};
// Upload to a time-limited S3 presigned URL (expires in 7 days)
const key = `gdpr-exports/${userId}/${Date.now()}.json`;
await s3.putObject({
Bucket: process.env.GDPR_EXPORT_BUCKET!,
Key: key,
Body: JSON.stringify(exportData, null, 2),
ContentType: "application/json",
// Auto-delete after 30 days
Expires: new Date(Date.now() + 30 * 24 * 3600 * 1000),
});
const downloadUrl = await s3.getSignedUrlPromise("getObject", {
Bucket: process.env.GDPR_EXPORT_BUCKET!,
Key: key,
Expires: 7 * 24 * 3600, // URL valid for 7 days
});
return downloadUrl;
}
š” The Difference Between a SaaS Demo and a SaaS Business
Anyone can build a demo. We build SaaS products that handle real load, real users, and real payments ā with architecture that does not need to be rewritten at 1,000 users.
- Multi-tenant PostgreSQL with row-level security
- Stripe subscriptions, usage billing, annual plans
- SOC2-ready infrastructure from day one
- We own zero equity ā you own everything
GDPR Engineering Checklist
Data Inventory:
ā” All PII data stores documented
ā” Data retention periods defined per table
ā” Third-party processors listed in DPA
Right to Erasure:
ā” Deletion pipeline covers all data stores
ā” Third-party deletion notifications implemented
ā” Erasure requests logged with completion status
ā” Partial erasure triggers alert for manual review
Consent:
ā” Consent stored as append-only event log
ā” Consent text + version recorded at time of consent
ā” Analytics disabled when consent revoked
ā” Consent UI surfaces in settings for self-service update
DSAR:
ā” Export covers all personal data
ā” Export delivered within 30 days (legal requirement)
ā” Export download link time-limited
Data Minimization:
ā” IP addresses hashed or truncated in analytics
ā” Logs don't contain raw emails or names
ā” Telemetry uses anonymous IDs, not emails
See Also
- SaaS Security Checklist ā security alongside compliance
- Data Privacy Engineering ā privacy by design patterns
- SaaS Analytics Architecture ā anonymous event design
- Multi-Tenant SaaS Architecture ā tenant data isolation
Working With Viprasol
GDPR compliance is easier to build correctly from the start than to retrofit into an existing system. We design deletion pipelines, consent management schemas, DSAR automation, and data minimization patterns for SaaS products serving EU users ā so compliance is a system property, not a manual process.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Building a SaaS Product?
We've helped launch 50+ SaaS platforms. Let's build yours ā fast.
Free consultation ⢠No commitment ⢠Response within 24 hours
Add AI automation to your SaaS product?
Viprasol builds custom AI agent crews that plug into any SaaS workflow ā automating repetitive tasks, qualifying leads, and responding across every channel your customers use.