ginkelsoft/laravel-data-consent

A Laravel package that implements GDPR art. 6(1)(a) and art. 7 consent recording as a tamper-evident append-only event log, with grant/withdraw actions and a status helper for the current state.

Maintainers

Package info

github.com/ginkelsoft-development/laravel-data-consent

pkg:composer/ginkelsoft/laravel-data-consent

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-05-28 17:13 UTC

This package is auto-updated.

Last update: 2026-05-28 17:15:44 UTC


README

Tests License Laravel PHP PHPStan

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 grant calls in a row produce two granted rows. Sometimes that is exactly what you want (re-affirming consent). When you want "only when not currently granted" semantics, check ConsentStatus::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 register ConsentEntry as 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:

  1. Versions — PHP, Laravel, and the package version (composer show ginkelsoft/laravel-data-consent).
  2. What you did — the artisan command, code snippet, or steps that triggered the bug.
  3. What you expected vs what actually happened — include full error output or a stack trace if there is one.
  4. 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.comhttps://ginkelsoft.com.

License

MIT License — see LICENSE. (c) 2026 Ginkelsoft