<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Felix Kruger</title>
    <description>The latest articles on DEV Community by Felix Kruger (@felixkruger).</description>
    <link>https://dev.to/felixkruger</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3842101%2F5f359a99-71f8-4a6c-82b2-c91a4754a060.png</url>
      <title>DEV Community: Felix Kruger</title>
      <link>https://dev.to/felixkruger</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/felixkruger"/>
    <language>en</language>
    <item>
      <title>The Complete GDPR Compliance Checklist for SaaS Developers (2026)</title>
      <dc:creator>Felix Kruger</dc:creator>
      <pubDate>Tue, 24 Mar 2026 19:04:37 +0000</pubDate>
      <link>https://dev.to/felixkruger/the-complete-gdpr-compliance-checklist-for-saas-developers-2026-40o4</link>
      <guid>https://dev.to/felixkruger/the-complete-gdpr-compliance-checklist-for-saas-developers-2026-40o4</guid>
      <description>&lt;p&gt;If you're building a SaaS product that serves EU customers — particularly in Germany and Austria — you need to take the GDPR seriously. The GDPR (General Data Protection Regulation, known as DSGVO in German: &lt;em&gt;Datenschutz-Grundverordnung&lt;/em&gt;) applies to any company processing personal data of people in the EU, regardless of where your company is based.&lt;/p&gt;

&lt;p&gt;I've audited dozens of SaaS products over the past year, and the same compliance gaps show up again and again. This guide covers the five critical areas you need to get right, with code examples showing the mistakes I see most often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GDPR Matters for Developers
&lt;/h2&gt;

&lt;p&gt;Let's be blunt: fines under the GDPR can reach up to 20 million euros or 4% of annual global turnover, whichever is higher. But beyond fines, EU users — especially German ones — are privacy-conscious. A missing legal notice or a broken cookie banner will tank your credibility faster than a 500 error on your landing page.&lt;/p&gt;

&lt;p&gt;In Germany, there's an additional risk: &lt;em&gt;Abmahnung&lt;/em&gt; — a formal legal warning letter, often sent by competitors or privacy advocacy groups. These are essentially cease-and-desist notices that come with a bill, typically ranging from a few hundred to several thousand euros. They're common, and they target low-hanging compliance failures like a missing &lt;em&gt;Impressum&lt;/em&gt; or an invalid cookie banner.&lt;/p&gt;

&lt;p&gt;The good news: if you handle these five areas correctly, you're covering about 90% of what a typical SaaS needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Impressum (Legal Notice)
&lt;/h2&gt;

&lt;p&gt;If you're serving customers in Germany or Austria, you need an &lt;em&gt;Impressum&lt;/em&gt; — a legally mandated identification page. This isn't optional — it's required under Section 5 of the German TMG (Telemediengesetz, the Telemedia Act). There's no exact equivalent in US or UK law; think of it as a mandatory "About Us" page with legally prescribed content.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Must Be Included
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Full legal name (company or individual)&lt;/li&gt;
&lt;li&gt;Physical postal address (no P.O. boxes)&lt;/li&gt;
&lt;li&gt;Contact email and phone number&lt;/li&gt;
&lt;li&gt;Commercial register number (if applicable)&lt;/li&gt;
&lt;li&gt;VAT identification number (&lt;em&gt;Umsatzsteuer-ID&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Responsible person for editorial content (if applicable)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common Mistake: Hiding It in the Footer
&lt;/h3&gt;

&lt;p&gt;Your &lt;em&gt;Impressum&lt;/em&gt; must be reachable within &lt;strong&gt;two clicks&lt;/strong&gt; from any page. A common anti-pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD: Impressum buried in a collapsed accordion&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;footer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Accordion&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AccordionItem&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Legal Stuff"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/impressum"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Impressum&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AccordionItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Accordion&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;footer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, place it as a direct link in your footer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: Direct, visible link&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;footer&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-4 text-sm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/impressum"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Impressum&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/privacy"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Privacy Policy&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;footer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pro Tip
&lt;/h3&gt;

&lt;p&gt;If you're a solo developer outside Germany, you still need an &lt;em&gt;Impressum&lt;/em&gt; if you target German users. You can use a business address service (&lt;em&gt;Geschäftsadresse&lt;/em&gt;) to avoid publishing your home address. Just make sure the address accepts physical mail.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Datenschutzerklärung (Privacy Policy)
&lt;/h2&gt;

&lt;p&gt;A &lt;em&gt;Datenschutzerklärung&lt;/em&gt; (literally: "data protection declaration") is the GDPR-compliant privacy policy. It goes well beyond the vague privacy policies common on US websites. German and Austrian courts have specific expectations about structure and content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Required Sections
&lt;/h3&gt;

&lt;p&gt;Your privacy policy must cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity of the data controller&lt;/strong&gt; (name, address, contact)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Protection Officer&lt;/strong&gt; contact (required if you process data as a core activity or have 20+ employees regularly processing personal data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal basis for each processing activity&lt;/strong&gt; (Art. 6 GDPR)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Categories of data collected&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purpose of processing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data retention periods&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party data processors&lt;/strong&gt; (with specific names — not just "analytics providers")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data subject rights&lt;/strong&gt; (access, rectification, erasure, portability, objection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right to lodge a complaint&lt;/strong&gt; with the supervisory authority&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common Mistake: Generic Legal Basis
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- BAD: Vague legal basis --&amp;gt;&lt;/span&gt;
We process your data based on legitimate interest.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to specify the legal basis &lt;strong&gt;per processing activity&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- GOOD: Specific legal basis per activity --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;### Analytics (Plausible Analytics)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Data collected: Page views, referrer, country
&lt;span class="p"&gt;-&lt;/span&gt; Legal basis: Art. 6(1)(f) GDPR (legitimate interest)
&lt;span class="p"&gt;-&lt;/span&gt; Legitimate interest: Understanding usage patterns to improve the service
&lt;span class="p"&gt;-&lt;/span&gt; Retention period: 24 months
&lt;span class="p"&gt;-&lt;/span&gt; Data processor: Plausible Insights OÜ, Estonia

&lt;span class="gu"&gt;### Account Registration&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Data collected: Email address, name
&lt;span class="p"&gt;-&lt;/span&gt; Legal basis: Art. 6(1)(b) GDPR (contract performance)
&lt;span class="p"&gt;-&lt;/span&gt; Retention period: Duration of account + 30 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Third-Party Services
&lt;/h3&gt;

&lt;p&gt;Every external service must be named explicitly. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analytics (Google Analytics, Plausible, Fathom)&lt;/li&gt;
&lt;li&gt;Payment processors (Stripe, Lemon Squeezy, PayPal)&lt;/li&gt;
&lt;li&gt;Email services (Resend, SendGrid, Mailchimp)&lt;/li&gt;
&lt;li&gt;CDNs (Cloudflare, Vercel, AWS CloudFront)&lt;/li&gt;
&lt;li&gt;Error tracking (Sentry, LogRocket)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your service transfers data outside the EU (e.g., to US-based providers), you must document the legal mechanism for that transfer — typically Standard Contractual Clauses (SCCs) or an EU adequacy decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Cookie Consent
&lt;/h2&gt;

&lt;p&gt;This is where most SaaS products fail. The GDPR, combined with the ePrivacy Directive (implemented in Germany as the TTDSG — &lt;em&gt;Telekommunikation-Telemedien-Datenschutz-Gesetz&lt;/em&gt;), requires &lt;strong&gt;prior consent&lt;/strong&gt; for any non-essential cookies or tracking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Requires Consent
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Analytics cookies (even "privacy-friendly" ones, if they set cookies)&lt;/li&gt;
&lt;li&gt;Marketing/advertising trackers&lt;/li&gt;
&lt;li&gt;Social media embeds&lt;/li&gt;
&lt;li&gt;A/B testing tools that use cookies&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Does NOT Require Consent
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Session cookies for authentication&lt;/li&gt;
&lt;li&gt;Shopping cart cookies&lt;/li&gt;
&lt;li&gt;Language preference cookies&lt;/li&gt;
&lt;li&gt;Cookies technically necessary for the service&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common Mistake: Pre-checked Consent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD: Pre-checked analytics checkbox&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;defaultChecked&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  Analytics cookies
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This violates the GDPR. Consent must be &lt;strong&gt;freely given&lt;/strong&gt;, meaning no pre-checked boxes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: Unchecked by default, clear labeling&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;analyticsConsent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setAnalyticsConsent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;
    &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;analyticsConsent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setAnalyticsConsent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  Analytics cookies — Help us understand how you use the site
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/privacy#analytics"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Learn more&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The "Reject All" Button
&lt;/h3&gt;

&lt;p&gt;EU regulators (and German courts in particular) have ruled that rejecting cookies must be &lt;strong&gt;as easy as&lt;/strong&gt; accepting them. That means your "Reject All" button must be as prominent as "Accept All":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// GOOD: Equal prominence for both options&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-3"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rejectAll&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Reject All
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;acceptAll&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Accept All
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note both buttons use the same variant. Making "Accept" a primary button and "Reject" a subtle text link is a dark pattern that regulators have specifically called out — in Germany, this can trigger an &lt;em&gt;Abmahnung&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading Scripts Conditionally
&lt;/h3&gt;

&lt;p&gt;Never load tracking scripts before consent is given:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD: Loading analytics unconditionally&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://analytics.example.com/script.js"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: Only load after consent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hasAnalyticsConsent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://analytics.example.com/script.js"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. DSAR Handling (Data Subject Access Requests)
&lt;/h2&gt;

&lt;p&gt;Under Art. 15 GDPR, any user can request a copy of all personal data you hold about them. You must respond within &lt;strong&gt;one month&lt;/strong&gt;. This right applies to all EU residents, not just German users.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Need
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A way to receive DSARs (email address or form)&lt;/li&gt;
&lt;li&gt;Identity verification before sharing data&lt;/li&gt;
&lt;li&gt;A process to export all user data&lt;/li&gt;
&lt;li&gt;The ability to delete user data on request (Art. 17 — the "Right to Erasure")&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Common Mistake: No Data Export
&lt;/h3&gt;

&lt;p&gt;If your app stores user data across multiple tables or services, you need a way to aggregate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API route: /api/dsar/export&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authenticateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activityLog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;supportTickets&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activityLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportTickets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;exportDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;activityLog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;supportTickets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Account Deletion
&lt;/h3&gt;

&lt;p&gt;Don't just soft-delete. If a user requests erasure under Art. 17, you need to actually remove their personal data (with some exceptions for legal retention requirements — for example, German tax law requires you to keep invoices for 10 years):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleErasureRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Delete personal data&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`deleted-&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{userId}@redacted.local&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;,
      name: "Deleted User",
      phone: null,
      address: null,
    },
  });

  // Keep anonymized order records (tax retention: 10 years in Germany)
  await db.orders.updateMany({
    where: { userId },
    data: { customerName: "Deleted User", customerEmail: null },
  });

  // Purge activity logs
  await db.activityLog.deleteMany({ where: { userId } });
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. DPA — Data Processing Agreement (AV-Vertrag)
&lt;/h2&gt;

&lt;p&gt;If you use any third-party service that processes personal data on your behalf, the GDPR (Art. 28) requires a signed Data Processing Agreement (DPA). In German, this is called an &lt;em&gt;Auftragsverarbeitungsvertrag&lt;/em&gt; or &lt;em&gt;AV-Vertrag&lt;/em&gt; — you'll see this term frequently when dealing with German-based providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Services That Require a DPA
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cloud hosting (Vercel, AWS, Hetzner)&lt;/li&gt;
&lt;li&gt;Email delivery (Resend, SendGrid)&lt;/li&gt;
&lt;li&gt;Payment processing (Stripe, Lemon Squeezy)&lt;/li&gt;
&lt;li&gt;Analytics (if data is processed by them)&lt;/li&gt;
&lt;li&gt;Customer support tools (Intercom, Zendesk)&lt;/li&gt;
&lt;li&gt;Error tracking (Sentry)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to Get One
&lt;/h3&gt;

&lt;p&gt;Most large providers offer a DPA you can sign online:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt;: Settings &amp;gt; Legal &amp;gt; DPA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt;: Available in the Stripe Dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS&lt;/strong&gt;: Part of the AWS Service Terms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hetzner&lt;/strong&gt;: Available in the Hetzner account area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Store these signed agreements.&lt;/strong&gt; If a supervisory authority audits you, they will ask for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Mistake: Forgetting Subprocessors
&lt;/h3&gt;

&lt;p&gt;Your DPA should list subprocessors. If Vercel uses AWS, and you have a DPA with Vercel, Vercel's agreement should cover their use of AWS. Check the subprocessor lists of your providers regularly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Checklist
&lt;/h2&gt;

&lt;p&gt;Here's your actionable checklist. Go through each item:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Impressum&lt;/strong&gt; visible and reachable within 2 clicks from every page&lt;/li&gt;
&lt;li&gt;[ ] Impressum contains full name, address, email, phone, register number&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Privacy Policy&lt;/strong&gt; (&lt;em&gt;Datenschutzerklärung&lt;/em&gt;) covers all data processing activities&lt;/li&gt;
&lt;li&gt;[ ] Each processing activity lists its legal basis (Art. 6 GDPR)&lt;/li&gt;
&lt;li&gt;[ ] All third-party services named with their purpose and location&lt;/li&gt;
&lt;li&gt;[ ] Data transfer mechanisms documented for non-EU processors (SCCs, adequacy decisions)&lt;/li&gt;
&lt;li&gt;[ ] Data retention periods specified per category&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Cookie consent&lt;/strong&gt; banner with equal-weight Accept/Reject buttons&lt;/li&gt;
&lt;li&gt;[ ] No tracking scripts loaded before consent&lt;/li&gt;
&lt;li&gt;[ ] Cookie preferences revocable at any time&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;DSAR process&lt;/strong&gt; documented and reachable&lt;/li&gt;
&lt;li&gt;[ ] Data export endpoint implemented&lt;/li&gt;
&lt;li&gt;[ ] Account deletion actually removes personal data&lt;/li&gt;
&lt;li&gt;[ ] Response process meets 30-day deadline&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;DPAs&lt;/strong&gt; signed with all data processors&lt;/li&gt;
&lt;li&gt;[ ] Subprocessor lists reviewed&lt;/li&gt;
&lt;li&gt;[ ] Agreements stored and accessible&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Skip the Research, Ship Compliant
&lt;/h2&gt;

&lt;p&gt;Putting all of this together from scratch takes days of research and legal review. If you want to skip ahead, I built the &lt;strong&gt;GDPR Starter Kit for Next.js&lt;/strong&gt; — a drop-in compliance package for Next.js SaaS projects targeting EU customers.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-built &lt;em&gt;Impressum&lt;/em&gt; and privacy policy templates (English/German)&lt;/li&gt;
&lt;li&gt;Cookie consent component with GDPR/TTDSG-compliant behavior&lt;/li&gt;
&lt;li&gt;DSAR handling API routes with data export and deletion&lt;/li&gt;
&lt;li&gt;DPA checklist and template&lt;/li&gt;
&lt;li&gt;All the code snippets from this article and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gdprtools.lemonsqueezy.com/checkout/buy/b2218d58-faf3-4dfe-bc47-eb2b94ee8448" rel="noopener noreferrer"&gt;Get the GDPR Starter Kit for Next.js — €79 →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions about GDPR compliance for your SaaS? Drop them in the comments — I'll do my best to help.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>gdpr</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
