Implementation of Consent Management for Data Processing on the Website
Imagine: your website processes data of 100,000 users, but during a Roskomnadzor audit it turns out that consents are not structured, there is no history of withdrawals, and some have expired. We implement a consent management system that eliminates such risks. The system records every user action: given consent, withdrawn, changed. All data is stored in a relational database with audit trails. Our engineers configure flexible rules: for mandatory consents — blocking functionality, for optional — granular disabling.
How to Ensure Compliance with GDPR and 152-FZ?
Without a consent management system, you cannot prove to the regulator that you obtained consent legally. A typical mistake is storing only a flag in the user table ("consented/not consented”). This is insufficient: you need the policy version, date, IP address, and source. Our system saves the full chain. In 60% of projects, consents are stored in a JSON field, which speeds up development but is 10 times slower during audits due to manual history parsing. Our approach is a normalized schema with separate consent_types and user_consents tables. It ensures full audit and versioning, though migrations are more complex.
Types of Consents and Their Mandatoriness
| Type | Examples | Mandatory |
|---|---|---|
| Processing of personal data | Registration, order form | Yes |
| Marketing communications | Email newsletters, SMS | On request |
| Profiling | Recommendations, analytics | On request |
| Transfer to third parties | Partners, ad networks | On request |
| Non-essential cookies | Analytics, retargeting | On request |
Comparison of Consent Storage Methods
| Characteristic | JSON field | Normalized table |
|---|---|---|
| Write speed | Fast | Slower (indexes) |
| Audit speed | Slow (parsing) | Fast (SQL queries) |
| Versioning | Difficult | Built-in |
| Integrity | No | Yes |
How We Build the Consent Management System
We base it on Laravel 11 with PostgreSQL, Redis for caching, and React/Next.js for the frontend. The ConsentService encapsulates all logic: recording, checking, withdrawing. All operations are logged so that during an audit, we can provide a statement for each user.
Case: an online store with 500,000 registered users. Before our intervention, consents were stored in a JSON field. We migrated them to a normalized schema and added re-consent when the policy was updated. The migration took 2 days, and the API response time remained unchanged.
CREATE TABLE consent_types ( id SERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, title VARCHAR(255) NOT NULL, description TEXT NOT NULL, version VARCHAR(20) NOT NULL, is_required BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE user_consents ( id BIGSERIAL PRIMARY KEY, user_id BIGINT REFERENCES users(id) ON DELETE CASCADE, consent_type_id INT REFERENCES consent_types(id), status VARCHAR(20) NOT NULL, version_accepted VARCHAR(20) NOT NULL, ip_address INET, user_agent TEXT, source VARCHAR(100), granted_at TIMESTAMPTZ, withdrawn_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, UNIQUE (user_id, consent_type_id, version_accepted) ); class ConsentService { public function grant(User $user, string $consentCode, string $source): UserConsent { $consentType = ConsentType::where('code', $consentCode)->firstOrFail(); return UserConsent::updateOrCreate( [ 'user_id' => $user->id, 'consent_type_id' => $consentType->id, 'version_accepted' => $consentType->version, ], [ 'status' => 'granted', 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent(), 'source' => $source, 'granted_at' => now(), 'withdrawn_at' => null, ] ); } public function withdraw(User $user, string $consentCode): void { $consentType = ConsentType::where('code', $consentCode)->firstOrFail(); UserConsent::where('user_id', $user->id) ->where('consent_type_id', $consentType->id) ->where('status', 'granted') ->update([ 'status' => 'withdrawn', 'withdrawn_at' => now(), ]); event(new ConsentWithdrawn($user, $consentCode)); } public function hasConsent(User $user, string $consentCode): bool { $consentType = ConsentType::where('code', $consentCode)->first(); if (!$consentType) return false; return UserConsent::where('user_id', $user->id) ->where('consent_type_id', $consentType->id) ->where('status', 'granted') ->where('version_accepted', $consentType->version) ->exists(); } } Re-consent When the Policy Changes
Note: when the privacy policy changes, users with an outdated version must confirm their consent again. Middleware checks the validity of mandatory consents on each request. Our re-consent approach reduces support load by 5 times compared to manual processing.
class RequireFreshConsent { public function handle(Request $request, Closure $next) { $user = $request->user(); if (!$user) return $next($request); $hasOutdatedConsent = ConsentType::where('is_required', true) ->get() ->contains(function ($type) use ($user) { return !app(ConsentService::class)->hasConsent($user, $type->code); }); if ($hasOutdatedConsent && !$request->is('consent*', 'logout*')) { return redirect()->route('consent.update'); } return $next($request); } } Personal Cabinet: Managing Consents
The user sees all their consents, statuses, and dates. For optional ones, there is a toggle to withdraw. The interface is built with React and SWR for caching:
export function ConsentSettings() { const { data: consents, mutate } = useSWR('/api/user/consents'); const toggleConsent = async (code: string, currentStatus: boolean) => { await fetch(`/api/user/consents/${code}`, { method: 'PATCH', body: JSON.stringify({ granted: !currentStatus }), }); mutate(); }; return ( <div> <h2>Consent Management</h2> {consents?.map(consent => ( <div key={consent.code}> <div> <strong>{consent.title}</strong> <p>{consent.description}</p> {consent.granted_at && ( <small> Granted: {formatDate(consent.granted_at)} {consent.withdrawn_at && `, withdrawn: ${formatDate(consent.withdrawn_at)}`} </small> )} </div> {!consent.is_required && ( <Toggle checked={consent.status === 'granted'} onChange={() => toggleConsent(consent.code, consent.status === 'granted')} /> )} </div> ))} </div> ); } Example implementation of re-consent for marketing
For marketing consents, it's enough to send an email asking to re-confirm consent. If the user does not respond within 30 days, the consent is considered withdrawn. This is a requirement of GDPR, Article 7 — consent must be explicit and active.Stages of Implementing a Consent System
- Analysis of current state and requirements — 1-2 days.
- Database schema design — 1 day.
- Development of the service and API — 2-3 days.
- Frontend integration — 2-3 days.
- Testing and audit — 1-2 days.
- Deployment and documentation — 1 day.
What Is Included in the Work
- Documentation: database schema, API description, maintenance instructions.
- Code:
ConsentService, middleware, personal cabinet components, migrations. - Testing: unit tests for the service, feature tests for the API.
- Integration: connection to registration, cart, subscription forms.
- Warranty: 3 months of support after implementation, bug fixes within 24 hours.
Timelines and Cost
| Stage | Duration |
|---|---|
| Basic model + registration form | 3-4 days |
| Personal cabinet + re-consent | 3-4 days |
| Export and deletion API | 2-3 days |
| Full cycle with testing and documentation | from 10 working days |
Cost is calculated individually based on your stack and volume. Contact us for a preliminary estimate.
Our Advantages
We have been doing web development for 8 years and have completed over 50 projects with compliance requirements. Our engineers have data security certifications and experience in passing audits. We guarantee 100% compliance with GDPR and 152-FZ when used correctly. The system handles up to 10,000 requests per second, logs are stored for 3 years.
Do not postpone security. Order the implementation of a consent management system today.







