GDPR Compliant Contact Form Implementation: A Technical Guide
To build a GDPR-compliant contact form, add a mandatory consent checkbox with specific purpose language, set a defined retention period (e.g., 30 days) with automatic deletion, and implement endpoints for data access and erasure requests. Below you will find working HTML, JavaScript, and Python code for each requirement, from consent capture through automated data cleanup.
Core GDPR Requirements for Contact Forms
The GDPR establishes specific requirements for processing personal data through contact forms:
You need a valid lawful basis for processing (typically consent or legitimate interest). Data must be used only for stated purposes (purpose limitation) and you should collect only what you need (data minimization). Delete data when no longer needed (storage limitation). Users can request their data (right to access) and request deletion (right to erasure).
For a basic contact form, consent is the most straightforward lawful basis, but you must implement it correctly.
Implementation Strategy
Form HTML Structure
A GDPR-compliant contact form starts with proper HTML markup:
<form id="contact-form" action="/api/contact" method="POST">
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
id="name"
name="name"
required
minlength="2"
maxlength="100"
>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
required
maxlength="254"
>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
name="message"
required
maxlength="5000"
rows="5"
></textarea>
</div>
<div class="consent-group">
<input
type="checkbox"
id="consent"
name="consent"
required
>
<label for="consent">
I consent to my data being processed for responding to this inquiry.
My data will be stored for 30 days and then deleted.
</label>
</div>
<button type="submit">Send Message</button>
</form>
Key points: required fields use HTML5 validation, field lengths are constrained, and the consent checkbox is mandatory with clear language.
Client-Side Validation
Add JavaScript for enhanced validation and consent confirmation:
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const consent = formData.get('consent');
if (!consent) {
alert('Please provide your consent to process your data.');
return;
}
// Collect consent timestamp
const submissionData = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
consent_given: true,
consent_timestamp: new Date().toISOString(),
ip_address: await getClientIP(),
user_agent: navigator.userAgent
};
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submissionData)
});
if (response.ok) {
window.location.href = '/thank-you/';
}
} catch (error) {
console.error('Submission failed:', error);
}
});
async function getClientIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
return data.ip;
} catch {
return null;
}
}
Server-Side Processing
Your backend must handle data securely and maintain compliance:
from datetime import datetime, timedelta
import json
import sqlite3
class ContactFormHandler:
def __init__(self, db_path='contact_submissions.db'):
self.db_path = db_path
self.init_database()
def init_database(self):
conn = sqlite3.connect(self.db_path)
conn.execute('''
CREATE TABLE IF NOT EXISTS submissions (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT,
message TEXT,
consent_given BOOLEAN,
consent_timestamp TEXT,
ip_address TEXT,
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
)
''')
conn.commit()
conn.close()
def process_submission(self, data):
# Validate consent
if not data.get('consent_given'):
raise ValueError('Consent is required')
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Store submission with 30-day retention
cursor.execute('''
INSERT INTO submissions
(name, email, message, consent_given, consent_timestamp, ip_address, user_agent, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', '+30 days'))
''', (
data['name'],
data['email'],
data['message'],
data['consent_given'],
data['consent_timestamp'],
data.get('ip_address'),
data.get('user_agent')
))
conn.commit()
conn.close()
return {'status': 'success'}
def handle_deletion_request(self, email):
"""Handle GDPR right to erasure"""
conn = sqlite3.connect(self.db_path)
# Find and anonymize matching records
cursor = conn.execute(
"SELECT id FROM submissions WHERE email = ?",
(email,)
)
records = cursor.fetchall()
for record in records:
conn.execute(
"UPDATE submissions SET name = '[deleted]', message = '[deleted]' WHERE id = ?",
(record[0],)
)
conn.commit()
conn.close()
return {'status': 'erased', 'records_affected': len(records)}
def cleanup_expired(self):
"""Remove data past retention period"""
conn = sqlite3.connect(self.db_path)
cursor = conn.execute(
"DELETE FROM submissions WHERE deleted_at < datetime('now')"
)
deleted_count = cursor.rowcount
conn.commit()
conn.close()
return {'deleted': deleted_count}
This implementation includes automatic cleanup of data past the 30-day retention period and handles deletion requests by anonymizing records rather than immediate removal (preserving audit trails).
Consent Best Practices
Separate Consent for Different Purposes
If you want to use contact form data for marketing, you need explicit separate consent:
<div class="consent-group">
<input type="checkbox" id="consent_response" name="consent_response" required>
<label for="consent_response">
I consent to receiving a response to my inquiry.
</label>
</div>
<div class="consent-group">
<input type="checkbox" id="consent_newsletter" name="consent_newsletter">
<label for="consent_newsletter">
I would like to receive newsletters and updates (optional).
</label>
</div>
Consent Records
Maintain detailed consent logs:
def log_consent_event(email, consent_type, granted, ip_address):
conn = sqlite3.connect('consent_log.db')
conn.execute('''
INSERT INTO consent_log (email, consent_type, granted, ip_address, timestamp)
VALUES (?, ?, ?, ?, ?)
''', (email, consent_type, granted, ip_address, datetime.now()))
conn.commit()
conn.close()
This creates an auditable record showing exactly when and how consent was obtained.
Data Subject Rights Implementation
GDPR grants users specific rights you must support:
Right to Access
def get_user_data(email):
conn = sqlite3.connect('contact_submissions.db')
cursor = conn.execute(
"SELECT * FROM submissions WHERE email = ?",
(email,)
)
records = cursor.fetchall()
conn.close()
return {
'email': email,
'submissions': records,
'consent_history': get_consent_history(email)
}
Right to Erasure
Your deletion handler should:
- Verify the requester’s identity
- Remove or anonymize all personal data
- Confirm completion within 30 days
- Notify any third parties who received the data
Security Considerations
Beyond GDPR compliance, secure your contact form against abuse:
- Implement rate limiting (e.g., maximum 5 submissions per hour per IP)
- Add CAPTCHA for spam protection
- Use HTTPS exclusively
- Validate and sanitize all input server-side
- Log access to personal data for accountability
from functools import wraps
import time
def rate_limit(max_requests=5, window_seconds=3600):
requests_log = {}
def decorator(func):
@wraps(func)
def wrapper(ip_address, *args, **kwargs):
now = time.time()
if ip_address in requests_log:
requests_log[ip_address] = [
t for t in requests_log[ip_address]
if now - t < window_seconds
]
if len(requests_log.get(ip_address, [])) >= max_requests:
raise ValueError('Rate limit exceeded')
requests_log.setdefault(ip_address, []).append(now)
return func(ip_address, *args, **kwargs)
return wrapper
return decorator
Compliance Checklist
Before deploying your contact form, verify:
- Clear consent checkbox with specific language
- Purpose stated for data processing
- Retention period defined and implemented
- Data subject access request endpoint exists
- Data deletion/anonymization endpoint exists
- Consent logging enabled
- Automatic cleanup scheduled
- Rate limiting and spam protection
- HTTPS enforced
- Privacy policy updated with contact form details
The implementation above covers the core technical requirements; consult legal counsel for jurisdiction-specific details such as those under the UK GDPR or CCPA.
Related Reading
- Bitwarden Vault Export Backup Guide: Complete Technical.
- GDPR Joint Controller Agreement Template: A Developer Guide
- VPN Warrant Canary: What It Means and Why It Matters
Built by theluckystrike — More at zovo.one