soved/laravel-gdpr
A GDPR compliance toolkit for Laravel: let users download or delete their personal data, anonymize and encrypt stored personal fields, manage consent, and log data access—so your app meets EU privacy rules without rebuilding core flows.
The package is a single-install compliance toolkit — install it, publish its config and migrations, then opt in models one at a time. The first 10 minutes should get you to a working consent log and a working data export.
composer require soved/laravel-gdpr
php artisan vendor:publish --provider="Soved\Laravel\Gdpr\GdprServiceProvider"
php artisan migrate
As of v1.10.0 the package supports Laravel 13.x (added via the Laravel Shift compatibility PR). If you're on Laravel 13, require ^1.10 explicitly — earlier tags (v1.9.x and below) will fail composer update on framework constraint conflicts. On Laravel 11/12, ^1.9 still works but there's no reason to stay behind.
The publish step drops three things you should look at in this order:
config/gdpr.php — the single source of truth for what gets anonymized and what gets exported. Read every key before you touch anything else; almost every gotcha in this package comes back to this file.gdpr_consents, gdpr_anonymizations, and gdpr_exports, plus a gdpr_rating (or similar) column on users. The rating column is your knobs for "how identifiable is this user, 1–5"; the package uses it to decide when to anonymize (>= 3 by default, configurable).The first use case to prove end-to-end is consent logging:
// In your cookie banner handler (e.g. a POST endpoint)
Gdpr::consent($user, 'marketing'); // or $user->giveConsent('marketing')
Gdpr::withdrawConsent($user, 'marketing');
Gdpr::hasConsent($user, 'marketing'); // bool
If that round-trips to the gdpr_consents table with the right purpose and consentable_type, the rest of the package will work. Don't move on to anonymization until it does.
Once consent works, add the HasGdpr (or equivalent) trait to App\User — most of the facade convenience hangs off it.
Consent is the workhorse. Every project that adopts this package ends up with two consent surfaces: a legal one (privacy policy, terms) and a processing one (marketing, analytics, third parties). Model purposes explicitly, don't conflate them — the whole point of a consent table is per-purpose granularity. Typical pattern:
// On registration
Gdpr::consent($user, 'privacy-policy');
// In an API endpoint hit by the banner
Gdpr::consent($user, request('purpose')); // validate purpose against an allowlist
Store the text version alongside the consent (or the policy URL hash) — auditors will ask. If the package doesn't give you that column out-of-the-box, add a nullable version to the consent migration; it won't break anything.
Anonymization is a workflow, not a call. The package separates "requested" from "done" via the gdpr_anonymizations table plus a scheduled command. The pattern the package intends:
php artisan gdpr:anonymize periodically and processes pending records.config/gdpr.php and nulls/overwrites them according to rating.You wire this into the scheduler. On Laravel 11+ that's routes/console.php (or bootstrap/app.php with ->withSchedule()); on older skeletons it's app/Console/Kernel.php:
$schedule->command('gdpr:anonymize')->hourly();
For synchronous tests or smaller apps you can call the anonymization directly — but do the async path in production, or a slow export will block your web request.
Data export is a job, not a controller response. Even the "instant" path streams a file. Two patterns in the wild:
gdpr_rating above threshold, archived to S3 for the retention window.Use the second if you have regulators breathing down your neck; use the first if you're a small team. Don't do both — you'll double your storage and confuse retention policy.
The compliance checklist is your onboarding doc. php artisan gdpr:checklist prints the package author's opinion of what "GDPR compliant" means. Treat it as a starting checklist, not a certification. Run it before and after wiring things up; gaps are visible immediately.
Ratings drive everything. Think of gdpr_rating as a sensitivity tag:
| Rating | Meaning | Typical fields |
|---|---|---|
| 1 | Public | display name |
| 2 | Low | city, job title |
| 3 | Medium | email, phone |
| 4 | High | address, IP |
| 5 | Critical | national ID, payment data |
Anonymization replaces fields with null or a placeholder based on which rating level you tell it to strip to. Decide your strip level with legal, not by yourself.
Integrate with soft deletes carefully. If your users table uses SoftDeletes, an anonymized user still "exists" but with null fields. That's usually what you want (referential integrity preserved, personal data gone) — but it means deleted_at no longer means what your app thinks it means. Consider a separate anonymized_at column and a scope.
v1.10.0 is framework compatibility (Laravel Shift PR #53) — no API changes, no migrations, no config keys. That means upgrading is low-risk, but it also means nothing about the package's behaviour changed: don't expect the Laravel 13 bump to fix the issues below. Budget a full regression run of consent + anonymization + export on the upgrade anyway, especially if you're also jumping framework major versions.enabled is a footgun in tests. Set GDPR_ENABLED=false in phpunit.xml so you don't accidentally write consents during unrelated feature tests — or, better, use RefreshDatabase and assert on consents explicitly. Half the "flaky test" bug reports on this package are consent side-effects leaking between tests.5 "to be safe." That makes every scheduled anonymization nuke everything. Start with the conservative low value and increment intentionally, or you'll discover the config's meaning by destroying data.gdpr_rating column in users. If you have an existing users table with soft deletes, encrypted columns, or a settings JSON blob, double-check that the package's anonymizer knows how to null each of them — a JSON column that's nulled wholesale can break your app while technically complying.gdpr:anonymize is a console command; if you also run it via a job, ensure it's idempotent. The package tracks state on the gdpr_anonymizations row — verify that a re-run of an already-completed job is a no-op, not a double-anonymization (which is harmless but pollutes your logs and may re-trigger audit alerts).user_id, the package's per-model anonymization will succeed at the model layer but leave orphans — or, worse, the deletion step will fire a FK constraint error mid-job. Fix this in a migration batch before enabling the scheduler.toJson() loops. Stream per-model and chunk with ->cursor(). The package's default export is fine for small accounts; for real ones, swap the exporter.ON DELETE RESTRICT on consentable relationships so a user deletion can't silently wipe the history./gdpr prefix conventions. Also check middleware — the package typically doesn't ship auth on its routes; you'll want to add it in a Route::group wrapper.php artisan gdpr:checklist is the author's opinion, not a lawyer's. Use it as a ticket generator, not a compliance proof. Pair it with your DPO's actual review.models array (register every model that holds PII — not just User), the facade for programmatic calls, and the exporter/anonymizer bindings in the service provider. If you need to customize how a field is anonymized (e.g. sha256(email) instead of null), you subclass the anonymizer and swap the binding — don't fork the package.v1.10.0 Laravel 13 bump is a single external PR), pin to a specific tag in composer.json and re-run your consent/anonymization tests on every upgrade. Treat each release as "compat-only until proven otherwise," and check the changelog for whether it touched behaviour at all — v1.10.0 did not.How can I help you explore Laravel packages today?