This boilerplate includes built-in GDPR compliance features: consent tracking, audit logs, data export, and account deletion. These features provide a foundation, but application-specific customization is required for full compliance.
- Overview
- GDPR Features
- Consent Tracking
- Audit Log
- Data Export
- Account Deletion
- Privacy Policy
- Cookies
- Customization Required
- API Reference
The General Data Protection Regulation (GDPR) is a European Union regulation that requires organizations to:
- Obtain consent before collecting personal data
- Provide transparency about data collection and usage
- Allow data access (Right of Access)
- Allow data correction (Right to Rectification)
- Allow data deletion (Right to Erasure / "Right to be Forgotten")
- Allow data portability (Right to Data Portability)
- Maintain audit logs for accountability
This boilerplate provides:
- ✅ Consent tracking with IP and timestamp
- ✅ Audit log for data subject actions
- ✅ Data export (download my data as JSON)
- ✅ Account deletion with cascading cleanup
- ✅ Privacy policy template
- ✅ Cookie notice
- ✅ Session tracking (IP address, user agent)
While these features provide a strong foundation, full GDPR compliance depends on:
- Your specific business requirements
- The types of data you collect
- How you process and store data
- Your data retention policies
- Third-party services you integrate
Recommendation: Consult with a legal professional to ensure full compliance for your specific use case.
Users can download all their personal data as a JSON file via the "Download My Data" button in their profile settings.
Included Data:
- User account information (name, email, email verified status, profile picture)
- Consent information (consent given, timestamp, IP address)
- Account creation and update timestamps
- Linked authentication providers (Google, email/password)
- Active sessions (IP address, user agent, creation time, expiry)
- User profile information
Users can update their personal information via the profile settings page:
- Name
- Profile picture
Additional fields can be added as needed.
Users can permanently delete their account and all associated data via the "Delete Account" button.
What Gets Deleted:
- User account record
- All sessions
- All authentication accounts (Google OAuth tokens, passwords, etc.)
- User profile
- Tenant memberships
- Verification tokens (email verification, OTP, password reset)
- OAuth tokens (encrypted access/refresh tokens)
What Remains:
- Audit log entries (user ID + action only, no personal data)
- This is required for legal accountability and regulatory compliance
The data export feature provides data in a machine-readable format (JSON), allowing users to transfer their data to another service.
Automatic consent is recorded when a user creates an account:
- User signs up (via any auth method)
- Better Auth creates user record
databaseHooks.user.create.afteris triggered- System records consent:
User.consentGiven = trueUser.consentAt = current timestampUser.consentIp = user's IP address
- Audit log entry created:
CONSENT_GIVEN
model User {
id String @id
email String
name String
consentGiven Boolean @default(false)
consentAt DateTime?
consentIp String?
// ...
}During sign-up, users see consent text:
"By signing in, you agree to our Privacy & Data Policy and consent to the processing of your personal data."
The privacy policy link is displayed prominently, and users must complete sign-up to proceed (implicit consent).
Note: Some jurisdictions require explicit opt-in consent (e.g., a checkbox). You may need to add this based on your requirements.
The GdprAuditLog table records all GDPR-related user actions:
| Action | Description |
|---|---|
CONSENT_GIVEN |
User created account and consented to data processing |
DATA_DELETION |
User requested and completed account deletion |
model GdprAuditLog {
id String @id @default(uuid())
userId String
action String @db.VarChar(50)
details String? @db.Text
ipAddress String?
createdAt DateTime @default(now())
@@index([userId])
@@map("gdpr_audit_log")
}Audit logs provide:
- Accountability: Proof of user actions and consent
- Regulatory compliance: Required by GDPR Article 30 (Records of Processing Activities)
- Transparency: Can be reviewed in case of disputes or investigations
Audit log entries are not deleted when a user deletes their account. This is intentional:
- Logs contain only
userId(a UUID) and action type - No personal data (name, email, etc.) is stored in logs
- Retention is necessary for legal accountability
- Logs may be purged after a statutory period (e.g., 7 years) based on your policies
- User navigates to "Profile & Security" settings
- Scrolls to "Data & Privacy" section
- Clicks "Download My Data"
- System generates JSON file with all user data
- Browser downloads
my-data-[timestamp].json
Backend endpoint: trpc.gdpr.myData.useQuery()
Exported Data Structure:
{
"user": {
"name": "John Doe",
"email": "john@example.com",
"emailVerified": true,
"image": "https://example.com/avatar.jpg",
"consentGiven": true,
"consentAt": "2024-01-01T00:00:00Z",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-02-01T00:00:00Z"
},
"accounts": [
{
"providerId": "google",
"scope": "openid email profile",
"createdAt": "2024-01-01T00:00:00Z"
}
],
"sessions": [
{
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"createdAt": "2024-01-15T00:00:00Z",
"expiresAt": "2024-01-22T00:00:00Z"
}
],
"profile": {
"createdAt": "2024-01-01T00:00:00Z"
}
}Security Notes:
- Sensitive data (passwords, OAuth tokens) is excluded from export
- Data is not stored on the server (generated on-demand)
- No authentication credentials included
- User navigates to "Profile & Security" settings
- Scrolls to "Data & Privacy" section
- Clicks "Delete Account"
- Modal appears with warning: "This action is irreversible"
- User must type their email address to confirm
- Account and all data is permanently deleted
- User is signed out and redirected to sign-in page
Backend endpoint: trpc.gdpr.deleteAccount.useMutation()
Deletion Flow:
- User submits deletion request with email confirmation
- System verifies confirmation matches user's email
beforeDeletehook executes:- Audit log entry created:
DATA_DELETION - Tenant memberships deleted
- Verification tokens deleted (email verification, OTP, password reset)
- Audit log entry created:
- Better Auth deletes user (CASCADE deletes all related records):
- Sessions
- Accounts (including OAuth tokens)
- User profile
- User is logged out
- Response returned to frontend
- Frontend redirects to sign-in page
Database Cascade:
model Session {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model UserProfile {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}A comprehensive privacy policy is included in the i18n files:
Location: apps/web/i18n/en/index.ts → Privacy section
Sections Covered:
- What data we collect
- How we use your data
- Cookies
- Third-party services
- How we protect your data
- Your rights (access, rectify, delete, withdraw consent)
- Data retention
- Contact information
The privacy policy is available at /privacy (or similar route in your app).
Users see links to the privacy policy:
- During sign-up
- In the footer
- In profile settings (Data & Privacy section)
The template policy is generic and must be customized for your application:
- Replace placeholder contact email
- Update service descriptions (Google Drive, AWS SES, Rollbar)
- Add any additional data collection points
- Add your company name and address
- Review with legal counsel
A cookie banner appears on first visit:
"This site uses essential cookies to keep you signed in. No tracking or advertising cookies."
Features:
- Non-intrusive design
- "Learn more" link to privacy policy
- "Got it" button to dismiss
- Preference stored in localStorage
| Cookie | Purpose | Expiry |
|---|---|---|
| Session Token | Keeps user signed in | 7 days |
| OAuth State | Secures Google sign-in flow | 3 days |
These are strictly necessary cookies (GDPR allows without explicit consent).
No tracking, analytics, or advertising cookies are used.
If you add analytics (e.g., Google Analytics), you must:
- Update cookie notice to mention analytics
- Obtain explicit consent (opt-in checkbox)
- Only load analytics after consent given
- Provide opt-out mechanism
While the boilerplate provides a solid foundation, you must customize based on your needs:
If you collect additional personal data (e.g., phone number, address, payment info), you must:
- Update consent text to mention these fields
- Include them in data export
- Delete them on account deletion
- Document them in privacy policy
If you integrate additional services (e.g., Stripe, Twilio, analytics), you must:
- Update privacy policy to mention them
- Update cookie notice if they use cookies
- Ensure they have data processing agreements (DPAs)
- Include their data in export (if stored)
Define and document:
- How long you retain user data
- How long you retain audit logs
- When inactive accounts are deleted
- When sessions are purged
If you transfer data outside the EU:
- Document this in the privacy policy
- Ensure adequate safeguards (e.g., Standard Contractual Clauses)
- Inform users about data transfer
If you use marketing emails or analytics:
- Obtain explicit opt-in consent (checkboxes during sign-up)
- Provide opt-out mechanisms
- Honor "Do Not Track" requests
- Update privacy policy
If your service is targeted at children under 16:
- Implement age verification
- Obtain parental consent
- Update privacy policy with special protections
const { data, isLoading } = trpc.gdpr.myData.useQuery();
if (data) {
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `my-data-${Date.now()}.json`;
a.click();
}const { mutate } = trpc.gdpr.updateProfile.useMutation();
mutate({
name: 'New Name',
image: 'https://example.com/new-avatar.jpg',
}, {
onSuccess: (updatedUser) => {
console.log('Profile updated:', updatedUser);
},
});const { mutate, isPending } = trpc.gdpr.deleteAccount.useMutation();
const handleDelete = () => {
if (window.confirm('Are you sure? This cannot be undone.')) {
mutate({
confirmation: userEmail, // Must match user's email
}, {
onSuccess: () => {
// User is logged out
router.push('/auth/signin');
},
onError: (error) => {
console.error('Deletion failed:', error.message);
},
});
}
};Clearly explain:
- What data you collect
- Why you collect it
- How long you keep it
- Who you share it with
GDPR rights should be easy to exercise:
- One-click data export
- Simple account deletion
- Clear privacy policy
Maintain records of:
- Data processing activities
- Consent collection
- Data breaches (if any)
- DPAs with third parties
GDPR compliance is ongoing:
- Review privacy policy annually
- Update consent text when adding features
- Audit third-party services
- Monitor regulatory changes
GDPR requires responses to data requests within 30 days (extendable to 60 days in complex cases).
Cause: Custom data not included in export query.
Solution: Update apps/api/src/modules/gdpr/gdpr.service.ts to include additional fields in the myData() method.
Cause: Foreign key constraint preventing deletion.
Solution: Ensure all related tables use onDelete: Cascade in Prisma schema:
model RelatedTable {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}Cause: databaseHooks.user.create.after is not executing.
Solution: Check Better Auth configuration in apps/api/src/modules/auth/auth.ts. Ensure the hook is registered.