Written by Huifer, solo developer and maintainer of TanStack Ship. I built a GDPR-aware SaaS in 2024 for a German B2B client, and the audit-ready patterns ended up as 3 default flows in TanStack Ship: consent logging with timestamps and source IP, EU data residency pinning via Cloudflare R2 jurisdiction and D1 region hints, and DSAR deletion with an audit trail the auditor could reconstruct. This post walks through each flow with working code, the engineering decisions behind them, and the boundaries I respect (this is engineering, not legal advice). The patterns survived one real audit and two follow-up compliance questionnaires.
Verified sources: GDPR Article 6 · GDPR Article 17 · GDPR Article 32 · EDPB Guidelines 05/2020 on Consent · ICO Consent Guidance · Cloudflare R2 Jurisdiction · Cloudflare D1 Region Hints · Stripe Data Processing Addendum · Better Auth Documentation · Paraglide JS
Last updated: 2026-05-02 · Changelog
TL;DR: A GDPR-ready SaaS boilerplate must ship three engineering flows that survive an auditor's first pass: (1) consent logging with timestamp, source IP, policy version, and withdrawal record; (2) data residency with EU-only processing for personal data via Cloudflare R2 jurisdiction and D1 regional hints; (3) DSAR + erasure with a 30-day endpoint that exports or deletes and writes an audit row the auditor can reconstruct. Working code below; named limits included.
GDPR-Ready SaaS Boilerplate: 3 Default Flows That Survived an Audit
Why GDPR is engineering, not just legal
Most GDPR content you will read is written by lawyers. This post is not. It is engineering, written by a developer who shipped the patterns below into a production SaaS that was audited by a German B2B customer's compliance team in Q4 2024. The audit passed. Two follow-up questionnaires in Q1 2026 also passed. The patterns are now defaults in TanStack Ship.
That said: this post is not legal advice. GDPR is enforced by supervisory authorities, not by boilerplate authors. Pair every pattern below with your own counsel, especially for DPIA drafting, legitimate-interest balancing, and sub-processor review. The patterns here are the engineering floor. The legal layer is on you.
The framing matters because the boilerplate market is full of vendors who claim "GDPR-ready" without distinguishing engineering from legal. A template can ship auditable code; it cannot ship your privacy policy, your DPIA, or your lawful-basis balancing test.
The 3 flows that fail audit
I have seen SaaS applications fail GDPR audits on three specific flows:
- No timestamped consent record. The auditor asked for the consent log for user X; the SaaS returned a checkbox. Consent must be a row in a database, not a flag on a user record. The IP and user agent columns are not optional.
- Personal data processed in the US region. The SaaS used a US-default database binding. The fix was not "add a clause to the privacy policy" — it was re-deploying with EU-only bindings, two weeks of refactoring.
- No DSAR endpoint. The auditor asked for the data export of user Y within the Article 12(3) 30-day deadline. The SaaS returned a support email. The clock had already started ticking.
A GDPR-ready boilerplate cannot solve the legal layer, but it can ship these three flows so you do not start from zero. The Q4 2024 audit produced a remediation list of 14 items; 11 were closed by wiring the patterns below, the remaining 3 were legitimate-interest balancing and sub-processor paperwork that no boilerplate could have fixed.
Flow 1: consent logging with audit trail
GDPR Article 6(1) requires a lawful basis for processing. For most SaaS, that basis is consent (6(1)(a)) or contract (6(1)(b)). The EDPB Guidelines 05/2020 on Consent require that consent be a freely-given, specific, informed, and unambiguous indication of the data subject's wishes. The engineering implication: consent must be demonstrable, timestamped, scoped, and withdrawable.
What consent data to store
The minimum auditable record: user_id, purpose, policy_version, granted_at (UTC), ip_address, user_agent, withdrawn_at (nullable), source. Each field answers a specific auditor question. The schema below is what ships in TanStack Ship's auth module.
-- migrations/0007_consent_log.sql — Cloudflare D1
CREATE TABLE consent_log (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
purpose TEXT NOT NULL,
policy_version TEXT NOT NULL,
granted_at INTEGER NOT NULL, -- unix epoch seconds, UTC
ip_address TEXT NOT NULL,
user_agent TEXT NOT NULL,
withdrawn_at INTEGER,
source TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_consent_user ON consent_log(user_id, purpose);
CREATE INDEX idx_consent_withdrawn ON consent_log(withdrawn_at)
WHERE withdrawn_at IS NOT NULL;
Every consent event is an insert: grant, withdrawal, and policy-version update all produce a row. The boilerplate wraps this in a server function so the handler code never forgets the IP and user agent.
Withdrawal is also a consent event
The ICO Consent Guidance is explicit that withdrawal must be as easy as granting. The withdrawn_at column above tracks the timestamp; the source column (settings_page, account_deletion, support_request) tells the auditor which surface the user used. Pre-built unsubscribe links in marketing emails write directly to this row, so the audit trail is consistent across surfaces.
Flow 2: EU data residency
Articles 44–50 restrict transfers of personal data outside the EEA unless an adequacy decision, Standard Contractual Clauses, or Binding Corporate Rules cover the transfer. The engineering implication: personal data must be processable in EU regions, which means your database binding, your object storage jurisdiction, and your session cache region all need to respect the EU-only boundary.
Regional bindings for D1 and R2
Cloudflare D1 supports regional hints via wrangler.jsonc. Cloudflare R2 supports jurisdiction selection at bucket creation. Both pinned to EU:
// wrangler.jsonc
{
"name": "tanstack-ship-eu",
"main": "src/server/index.ts",
"compatibility_date": "2025-09-01",
"placement": { "mode": "smart" },
"d1_databases": [
{
"binding": "DB",
"database_name": "tanstack-ship-prod",
"database_id": "<your-db-id>",
"regional_hint": "eu"
}
],
"r2_buckets": [
{
"binding": "UPLOADS",
"bucket_name": "tanstack-ship-uploads-eu",
"jurisdiction": "eu"
}
]
}
# One-time bucket creation — choose jurisdiction at creation time
wrangler r2 bucket create tanstack-ship-uploads-eu --jurisdiction eu
The combination — D1 regional hint eu, R2 jurisdiction eu, Workers smart placement — gives you an EU-only data path the auditor can verify by inspecting the bindings alone. No runtime check, no log scraping, no "trust us" statement.
Flow 3: DSAR export and erasure
Article 17 (right to erasure) and Article 20 (data portability) require that a data subject can request deletion or export within 30 days. The implementation is two server functions, both wrapped with an audit row.
Erasure endpoint with audit trail
Erasure is harder than export. GDPR allows legitimate-interest retention for some records (tax invoices, fraud logs), so the boilerplate anonymizes where retention is required and hard-deletes where it is not.
// src/server/dsar.ts
import { z } from "zod"
import { createServerFn } from "@tanstack/start"
import { eq } from "drizzle-orm"
import { db } from "~/db"
import { events, sessions, consentLog, users, dsarAudit } from "~/db/schema"
export const exportUserDataFn = createServerFn({ method: "POST" })
.validator(z.object({ userId: z.string() }))
.handler(async ({ data }) => {
// Audit row first — even an empty export is a DSAR event.
await db.insert(dsarAudit).values({
id: crypto.randomUUID(),
userId: data.userId,
kind: "export",
requestedAt: Math.floor(Date.now() / 1000),
completedAt: Math.floor(Date.now() / 1000),
})
const [profile, subs, events, consents] = await Promise.all([
db.select().from(users).where(eq(users.id, data.userId)),
db.select().from(subs).where(eq(subs.userId, data.userId)),
db.select().from(events).where(eq(events.userId, data.userId)),
db.select().from(consentLog).where(eq(consentLog.userId, data.userId)),
])
return { profile, subs, events, consents, exportedAt: new Date().toISOString() }
})
export const eraseUserDataFn = createServerFn({ method: "POST" })
.validator(z.object({ userId: z.string(), reason: z.string() }))
.handler(async ({ data }) => {
const requestedAt = Math.floor(Date.now() / 1000)
// Hard delete: events, sessions, consent_log (per Article 17).
await db.delete(events).where(eq(events.userId, data.userId))
await db.delete(sessions).where(eq(sessions.userId, data.userId))
await db.delete(consentLog).where(eq(consentLog.userId, data.userId))
// Anonymize: user row retained for tax/fraud but PII stripped.
await db.update(users)
.set({
email: `deleted-${data.userId}@anon.invalid`,
name: "Deleted User",
anonymizedAt: new Date(),
})
.where(eq(users.id, data.userId))
// Audit row — auditor will look for this first.
await db.insert(dsarAudit).values({
id: crypto.randomUUID(),
userId: data.userId,
kind: "erasure",
requestedAt,
completedAt: Math.floor(Date.now() / 1000),
reason: data.reason,
})
})
The audit row is the auditor's first stop. It answers who asked, when, and what you did. Without it, the auditor has to reconstruct the timeline from your application logs — a conversation you do not want to have.
Where boilerplate helps vs where it does not
The three flows above are engineering patterns: reproducible, testable, auditable. They cover roughly 60% of what a GDPR audit probes, and they are the part you can ship without a lawyer in the room.
The remaining 40% is legal text a boilerplate cannot ship:
- Privacy policy. You need a policy that matches the consent purposes you log. Mismatch here is a common audit finding — the policy says one thing, the code does another.
- DPIA (Data Protection Impact Assessment). Required for high-risk processing such as biometric data, children's data, or large-scale profiling. The risk profile is yours to define; boilerplate cannot draft it.
- Sub-processor list. Your Stripe, Resend, Better Auth, and Cloudflare contracts are your sub-processor list. Stripe's DPA and Cloudflare's enterprise terms are good starts; you need to maintain the list as you add vendors.
- Cross-border transfer mechanism. SCCs, adequacy decisions, BCRs — these are legal instruments, not config files.
- Breach notification process. Article 33 requires a 72-hour notification to the supervisory authority. The boilerplate can ship the runbook template; you need the legal contact on speed dial.
If a vendor sells you a "GDPR-ready SaaS starter" and claims it covers all of that, they are selling legal text they cannot legally deliver. Pair every engineering pattern with a privacy lawyer, and treat the boilerplate as the substrate, not the substitute.
The order I would recommend: ship the engineering floor first (3 flows + audit trail), book a 2-hour consult with a privacy lawyer to review your consent text and sub-processor list, then run an internal pre-audit walkthrough before your first EU enterprise customer. That sequence has held up across three SaaS apps I have shipped to EU customers since 2024.
The compliance checklist
A 10-point checklist the patterns above implement:
- Consent is timestamped and scoped (consent_log table) — Article 6(1)(a)
- Withdrawal is logged (withdrawn_at column) — Article 7(3)
- Policy version recorded (policy_version column) — Article 7(2)
- Source IP captured (ip_address column) — strengthens demonstrable consent
- D1 binding is EU region (regional_hint: eu) — restricts transfer
- R2 bucket jurisdiction is EU — restricts transfer for files
- Workers placement is smart — automatic EU execution
- DSAR export endpoint exists (exportUserDataFn) — Article 20
- Erasure endpoint exists (eraseUserDataFn) — Article 17
- DSAR audit trail is auditable (dsar_audit table) — Article 5(2) accountability
You still need a privacy lawyer, but you do not need to start from zero on the engineering side. The patterns above are the defaults in TanStack Ship.
Frequently asked questions
Does Cloudflare R2 jurisdiction guarantee GDPR compliance?
No. Jurisdiction restricts where data is stored at rest. You still need a lawful basis for processing, a sub-processor agreement with Cloudflare, and an SCC or adequacy decision if any data leaves the EU region. The Cloudflare DPA covers part of this; the rest is on your architecture.
What about legitimate interest instead of consent?
Legitimate interest (Article 6(1)(f)) is a valid basis that does not require consent, but it requires a documented balancing test. The boilerplate still logs the basis decision in the same consent_log table — naming it purpose="legitimate_interest_fraud_prevention" with a policy_version pointing at your internal balancing-test document. The auditor will ask for the document.
How long do I keep DSAR audit rows?
There is no GDPR-mandated retention for the audit itself, but supervisory authorities have held that you must keep it long enough to demonstrate compliance with the original request. The TanStack Ship default is 36 months, configurable per jurisdiction. The auditor never objected to this in the 2024 audit.
What about Stripe's role as a sub-processor?
Stripe's DPA is the contract you need. For EU-only personal data, you should also enable Stripe's EU-only processing in the dashboard (Settings → Data localization → EU). Without that flag, Stripe will route some customer data to US regions for fraud modeling. Disclose Stripe on your sub-processor page; auditors expect it.
Can I use this boilerplate for HIPAA or SOC 2 as well?
The audit-trail and access-control patterns overlap, but HIPAA in particular requires a Business Associate Agreement with every sub-processor (Cloudflare, Stripe, Resend) and SOC 2 requires a separate control framework. The DSAR and consent flows here are reusable; the legal paperwork is jurisdiction-specific. For HIPAA on Cloudflare, check the Cloudflare HIPAA compliance page before assuming the runtime is covered.
Is this post legal advice?
No. It is engineering. The patterns above are the engineering floor for the three flows most SaaS audits probe. Pair every pattern with a privacy lawyer for your specific use case, especially before signing enterprise contracts that include GDPR data-processing addenda.
Get started with TanStack Ship — the TanStack Start SaaS starter that ships all 3 GDPR flows by default: consent logging, EU data residency, and DSAR + erasure. See the features → or check Ship pricing →.