Skip to main content

Best SaaS Boilerplates for GDPR Compliance in 2026

·StarterPick Team
gdprprivacysaas-boilerplatecompliance2026

TL;DR

GDPR compliance isn't a plugin — it's an architectural choice. The decisions baked into your boilerplate (how auth is stored, where data lives, whether you can delete a user completely) determine how hard GDPR compliance will be. The best boilerplates for GDPR in 2026 are: Supastarter (Supabase RLS ensures data isolation; EU data center option), T3 Stack (you control every data store), and self-hosted Supabase + any boilerplate (data never leaves your servers). Here's what GDPR actually requires technically and how each boilerplate stacks up.

Key Takeaways

  • GDPR core requirements: right to deletion, right to access, data portability, consent management, breach notification
  • Boilerplate impact: data model (org isolation, cascade deletes) and hosting choice (EU servers)
  • Supastarter: best pre-built RLS isolation + EU Supabase region support
  • T3 Stack + self-hosted: maximum control, minimum third-party data sharing
  • The hard GDPR parts: deleting user data from all third-party integrations (analytics, CRM, email)
  • Cookie consent: all boilerplates need a consent manager added — none ship with one

What GDPR Actually Requires (Technically)

GDPR is a legal framework, but its requirements translate into concrete technical features:

GDPR RequirementTechnical Implementation
Right to erasure (Art. 17)CASCADE DELETE from all tables; purge from analytics, email, CRM
Right to access (Art. 15)Export all user data as JSON/CSV
Data portability (Art. 20)Machine-readable data export
Consent (Art. 7)Cookie consent manager; opt-in not pre-ticked
Data minimization (Art. 5)Don't store data you don't need
Breach notification (Art. 33)Incident response process, 72-hour notification
Data Processing AgreementsDPAs with all vendors (Stripe, SendGrid, etc.)
Privacy by design (Art. 25)Default settings protect privacy
EU data residencyOptional but often contractually required

The Four GDPR-Critical Technical Features

1. Complete User Data Deletion

The most technically challenging GDPR requirement is "right to erasure" — you must be able to delete a user and all their data from every system.

// Complete user deletion (what you need to implement in any boilerplate):
export async function deleteUserAndAllData(userId: string) {
  // Step 1: Cancel Stripe subscription
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { subscription: true },
  });

  if (user?.subscription?.stripeSubscriptionId) {
    await stripe.subscriptions.cancel(user.subscription.stripeSubscriptionId);
    await stripe.customers.del(user.subscription.stripeCustomerId);
  }

  // Step 2: Delete from email service (Resend/SendGrid)
  if (user?.resendContactId) {
    await resend.contacts.remove({ id: user.resendContactId, audienceId: AUDIENCE_ID });
  }

  // Step 3: Delete from analytics (PostHog)
  if (process.env.POSTHOG_API_KEY) {
    await fetch(`https://your-posthog.com/api/person/`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${process.env.POSTHOG_API_KEY}` },
      body: JSON.stringify({ distinct_ids: [userId] }),
    });
  }

  // Step 4: Delete files from storage (Supabase/S3)
  await supabase.storage.from('user-files').list(userId).then(async ({ data }) => {
    const paths = data?.map((f) => `${userId}/${f.name}`) ?? [];
    if (paths.length) await supabase.storage.from('user-files').remove(paths);
  });

  // Step 5: Delete from database (CASCADE deletes handle related records):
  await db.user.delete({ where: { id: userId } });

  // Step 6: Anonymize audit logs (delete identity, keep event)
  await db.auditLog.updateMany({
    where: { userId },
    data: { userId: null, email: '[deleted]', ipAddress: null },
  });
}

Why Prisma/Drizzle schemas matter for GDPR:

// ✅ GDPR-friendly schema — cascade deletes propagate:
model User {
  id            String   @id @default(cuid())
  email         String   @unique
  subscriptions Subscription[]
  sessions      Session[]
  posts         Post[]
  files         File[]
  // When user is deleted: all related records auto-delete
}

model Subscription {
  userId  String
  user    User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}

// ❌ GDPR-unfriendly — orphaned records after deletion:
model Subscription {
  userId  String
  user    User   @relation(fields: [userId], references: [id])
  // onDelete defaults to Restrict — deletion FAILS if subscription exists
}

2. Data Export (Right to Access)

// app/api/user/data-export/route.ts
export async function GET(req: Request) {
  const session = await auth();
  if (!session) return new Response('Unauthorized', { status: 401 });

  const userId = session.user.id;

  // Collect all user data:
  const [user, subscriptions, posts, files] = await Promise.all([
    db.user.findUnique({ where: { id: userId } }),
    db.subscription.findMany({ where: { userId } }),
    db.post.findMany({ where: { userId } }),
    db.file.findMany({ where: { userId } }),
  ]);

  const exportData = {
    exportDate: new Date().toISOString(),
    profile: {
      id: user?.id,
      email: user?.email,
      name: user?.name,
      createdAt: user?.createdAt,
    },
    subscriptions: subscriptions.map((s) => ({
      plan: s.plan,
      status: s.status,
      startDate: s.createdAt,
    })),
    content: posts,
    files: files.map((f) => ({ name: f.name, size: f.size, createdAt: f.createdAt })),
  };

  return new Response(JSON.stringify(exportData, null, 2), {
    headers: {
      'Content-Type': 'application/json',
      'Content-Disposition': `attachment; filename="my-data-${Date.now()}.json"`,
    },
  });
}

All boilerplates need a cookie consent manager added — none ship with one:

// Minimal consent manager (add to any boilerplate):
// components/ConsentBanner.tsx
'use client';
import { useState, useEffect } from 'react';

type ConsentPreferences = {
  necessary: true;          // Always true, can't be turned off
  analytics: boolean;
  marketing: boolean;
};

export function ConsentBanner() {
  const [showBanner, setShowBanner] = useState(false);
  const [prefs, setPrefs] = useState<ConsentPreferences>({
    necessary: true,
    analytics: false,
    marketing: false,
  });

  useEffect(() => {
    const stored = localStorage.getItem('consent');
    if (!stored) setShowBanner(true);
  }, []);

  const acceptAll = () => {
    const consent = { necessary: true, analytics: true, marketing: true };
    localStorage.setItem('consent', JSON.stringify(consent));
    setShowBanner(false);
    initAnalytics(); // Only load analytics after consent
  };

  const acceptNecessary = () => {
    const consent = { necessary: true, analytics: false, marketing: false };
    localStorage.setItem('consent', JSON.stringify(consent));
    setShowBanner(false);
  };

  if (!showBanner) return null;

  return (
    <div className="fixed bottom-0 left-0 right-0 bg-white border-t p-4 shadow-lg z-50">
      <p className="text-sm text-gray-600 mb-3">
        We use cookies to improve your experience. Analytics cookies help us understand
        how you use our product.{' '}
        <a href="/privacy" className="underline">Privacy policy</a>
      </p>
      <div className="flex gap-2">
        <button
          onClick={acceptAll}
          className="px-4 py-2 bg-black text-white rounded text-sm"
        >
          Accept all
        </button>
        <button
          onClick={acceptNecessary}
          className="px-4 py-2 border rounded text-sm"
        >
          Necessary only
        </button>
      </div>
    </div>
  );
}

// Only load analytics after consent:
export function initAnalytics() {
  if (typeof window === 'undefined') return;
  const consent = JSON.parse(localStorage.getItem('consent') ?? '{}');
  if (consent.analytics) {
    // Load PostHog, GA, etc.
    import('./analytics').then(({ loadPostHog }) => loadPostHog());
  }
}

Or use a third-party consent manager: CookieYes ($10/month), Cookiebot ($12/month), or Termly (free tier). These handle auto-blocking scripts based on consent — more reliable than DIY.

4. EU Data Residency

# Supabase — choose EU region at project creation:
# Europe (Frankfurt): eu-central-1
# Europe (London): eu-west-2

# Configure in .env:
NEXT_PUBLIC_SUPABASE_URL="https://[ref].supabase.co"
# Choose EU region during project creation in Supabase dashboard

# Vercel — set region:
# vercel.json:
{
  "regions": ["fra1"]  # Frankfurt
}

# Or in Vercel dashboard: Settings → Functions → Region → Frankfurt (fra1)

Boilerplate Rankings for GDPR

Supastarter — Best GDPR Foundation

Why it's best for GDPR:

  1. Row Level Security pre-written — data isolation between organizations is enforced at the database level (users can't access other orgs' data, even with SQL injection)
  2. Supabase EU region support — point to Frankfurt during setup
  3. Complete Prisma-style cascade deletes in migrations
  4. SSR auth means auth tokens never stored in localStorage (XSS safe)

What you still need to add:

  • Cookie consent manager (not included)
  • Data export endpoint
  • Complete deletion flow including Stripe/email service cleanup

T3 Stack — Most Controllable

Why T3 is good for GDPR:

  • Prisma schema fully in your control — design cascade deletes from day one
  • No mandatory third-party services baked in
  • Self-host on Railway/Render in EU region easily
  • NextAuth self-hosted = no user data at auth0.com/clerk.com

GDPR additions needed:

// Add to prisma/schema.prisma — cascade deletes:
model User {
  // Add to every relation:
  posts Post[] @relation(onDelete: "Cascade")
  files File[] @relation(onDelete: "Cascade")
  sessions Session[] @relation(onDelete: "Cascade")
}

ShipFast — Average GDPR Support

ShipFast doesn't have pre-written RLS (no multi-tenancy), but for B2C SaaS (one user = their own data), GDPR is actually simpler — you only need deletion, export, and consent.

ShipFast GDPR gaps:

  • MongoDB option: cascade deletes require middleware or manual deletion
  • No built-in data export
  • Analytics (Plausible) is privacy-friendly — points in ShipFast's favor

Self-Hosted Supabase + Any Boilerplate — Maximum Privacy

If GDPR compliance is a hard requirement and you have data sovereignty concerns:

# Self-host Supabase with Docker:
git clone https://github.com/supabase/supabase
cd supabase
cp .env.example .env
# Configure SMTP, JWT secret, etc.
docker compose up -d

# Your data never leaves your server
# You control every aspect of data processing
# GDPR Article 28 DPA needed with hosting provider, not Supabase

Third-Party Services Checklist

When building a GDPR-compliant SaaS, sign DPAs with every vendor:

ServiceDPA AvailableData LocationGDPR-Safe
SupabaseEU regions available
StripeEU regions
ResendUS (EU option)✅ with DPA
PostHog (self-hosted)N/AYour server
PostHog (cloud EU)EU
VercelEU regions
ClerkUS + EU✅ with DPA
Plausible (self-hosted)N/AYour server

The GDPR Setup Checklist for Any Boilerplate

Database:
[ ] Cascade deletes on all user relations (Prisma onDelete: Cascade)
[ ] No user PII in log files
[ ] Audit logs anonymized on deletion (keep event, remove identity)

API / Backend:
[ ] DELETE /api/user/account endpoint implemented
[ ] GET /api/user/data-export endpoint implemented
[ ] Auth tokens in httpOnly cookies (not localStorage)
[ ] No unnecessary data collected at signup

Frontend:
[ ] Cookie consent banner (not pre-checked boxes)
[ ] Analytics only loaded after consent
[ ] Privacy policy linked from signup and footer
[ ] Cookie policy listing all cookies used

Infrastructure:
[ ] EU region selected (Vercel fra1, Supabase eu-central-1)
[ ] DPAs signed with all vendors
[ ] Data retention policy defined (delete inactive accounts after X days)

Legal:
[ ] Privacy policy drafted (use Iubenda, Termly, or a lawyer)
[ ] Terms of service updated
[ ] Stripe DPA signed in Stripe dashboard
[ ] Supabase DPA signed in Supabase dashboard

Find boilerplates by compliance features at StarterPick.

Comments