laravel-data-consent maintained by ginkelsoft
Ginkelsoft Laravel Data Consent
Overview
Implements GDPR art. 6(1)(a) (consent as lawful basis) and art. 7
(conditions for consent — including the demonstrability requirement) for
a Laravel application. Records every grant and every withdrawal as an
append-only, hash-chained event in consent_log. The current state for a
given (subject, purpose, version) is derived from the latest event.
This is the consent member of the GinkelSoft compliance family. The
chain is built on the shared HashChain from
ginkelsoft/laravel-compliance-core and the signing secret is shared
with every other audit log in the family.
The family
| Package | GDPR Article(s) | Role |
|---|---|---|
laravel-compliance-core |
art. 5(2) | Shared primitives |
laravel-data-retention |
art. 5(1)(e) | Storage limitation |
laravel-data-right-to-be-forgotten |
art. 17 | Subject-driven erasure |
laravel-data-subject-access |
art. 15 + 20 | Subject access |
laravel-data-consent |
art. 6(1)(a) + 7 | Consent registry — this package |
laravel-data-breach-registry |
art. 33 + 34 | Breach registry |
laravel-compliance-hub |
art. 5(2) | Umbrella |
How it works
Record a grant or withdrawal
use Ginkelsoft\DataConsent\Actions\RecordConsent;
$consent = new RecordConsent;
$consent->grant(
subjectId: '01HXYZ',
purpose: 'newsletter',
version: '2026-05',
source: 'web',
metadata: ['ip' => '203.0.113.5', 'form' => 'signup-v3'],
);
$consent->withdraw(
subjectId: '01HXYZ',
purpose: 'newsletter',
version: '2026-05',
source: 'email',
);
version lets you tie consent to a specific consent text or processing
context. When you change your terms, prior consent does not automatically
cover the new version — record a fresh grant against the new version
string.
Query consent
use Ginkelsoft\DataConsent\Support\ConsentStatus;
$status = new ConsentStatus;
$status->isGranted('01HXYZ', 'newsletter');
$status->isGranted('01HXYZ', 'newsletter', version: '2026-05');
$status->latest('01HXYZ', 'newsletter');
$status->activeFor('01HXYZ');
$status->history('01HXYZ', purpose: 'newsletter');
activeFor returns only purposes whose latest event is granted —
perfect for an account dashboard that lists "what you currently consent
to".
CLI
For ops, backfills, and tests:
php artisan retention:consent:grant 01HXYZ newsletter --consent-version=2026-05 --source=web
php artisan retention:consent:withdraw 01HXYZ newsletter --consent-version=2026-05 --source=email
php artisan retention:consent:status 01HXYZ
The command names keep the retention:consent: prefix for BC with the
v1.x monolithic package. --consent-version rather than --version
because Symfony already uses --version as a reserved option.
Verify the chain
use Ginkelsoft\ComplianceCore\Config\LogSecret;
use Ginkelsoft\ComplianceCore\Support\HashChain;
use Illuminate\Support\Facades\DB;
$entries = DB::table('consent_log')->orderBy('id')->get()
->map(fn ($row) => (array) $row)->all();
$intact = HashChain::verify($entries, LogSecret::value());
Or run php artisan compliance:verify from the
hub to
verify every chain in the family in one shot.
What the log stores
consent_log is the only audit log in the family that stores the
subject identifier directly. Consent inherently requires identification —
you cannot prove "this person consented" without knowing who they are.
Document that in your DPIA and apply your own retention policy to this
table.
Per row: subject identifier, purpose, version, action (granted /
withdrawn), source, optional metadata, occurred-at timestamp, plus the
chain bookkeeping.
Compliance notes
- GDPR art. 6(1)(a) — Consent as a lawful basis for processing.
- GDPR art. 7 — Conditions for consent: demonstrability ("the controller shall be able to demonstrate that the data subject has consented"). The hash-chained log IS the demonstration.
- GDPR art. 5(2) — Accountability. Idem.
This package is not legal advice. Whether consent is freely given, specific, informed and unambiguous is a question for your DPO.
Installation
composer require ginkelsoft/laravel-data-consent
php artisan vendor:publish --tag=compliance-config
php artisan vendor:publish --tag=consent-config
php artisan vendor:publish --tag=consent-migrations
php artisan migrate
Then add a secret to .env (shared with the rest of the family):
COMPLIANCE_LOG_SECRET="$(openssl rand -base64 32)"
Gotchas
- The log stores subject identifiers directly. That is necessary for art. 7 accountability — proof of consent has to be linkable to a real person. Mention this table in your DPIA.
- No automatic deduplication. Two
grantcalls in a row produce twograntedrows. Sometimes that is exactly what you want (re-affirming consent). When you want "only when not currently granted" semantics, checkConsentStatus::isGranted()first. - Withdrawal does not delete the prior grant. It records a new event that supersedes it. The grant stays in the log forever — that is what makes the chain a usable audit trail.
- Forget does not automatically cascade to consent_log. A subject
exercising their right to be forgotten (via
laravel-data-right-to-be-forgotten) will not have their consent records removed unless you explicitly registerConsentEntryas Forgettable. The legal argument for keeping consent + withdrawal records even after forget is real (you may need them to defend the lawfulness of past processing), so this is opt-in rather than default.
Testing
composer install
vendor/bin/pest
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
Reporting bugs
Found a bug or unexpected behaviour? We want to hear about it.
Preferred — open a GitHub issue: https://github.com/ginkelsoft-development/laravel-data-consent/issues/new
When opening an issue, please include:
- Versions — PHP, Laravel, and the package version
(
composer show ginkelsoft/laravel-data-consent). - What you did — the artisan command, code snippet, or steps that triggered the bug.
- What you expected vs what actually happened — include full error output or a stack trace if there is one.
- A minimal reproduction if you can — a failing test or a small code sample beats a long description.
Security-sensitive findings (anything that could expose personal data, break a hash-chain, or bypass an audit log) — please do not open a public issue. E-mail info@ginkelsoft.com directly with "SECURITY" in the subject line and we will respond privately.
Not on GitHub? You can also e-mail info@ginkelsoft.com with the same information.
Contact
For commercial support, integration questions, or anything that doesn't fit a GitHub issue: info@ginkelsoft.com — https://ginkelsoft.com.
License
MIT License — see LICENSE. (c) 2026 Ginkelsoft