Laravel Package

Bewaartermijnen aantoonbaar handhaven in Laravel

Een Laravel package dat per Eloquent-model een bewaartermijn vastlegt, verlopen records verwijdert of anonimiseert en daar een fraude-bestendig audit-log van bijhoudt. Bedoeld om de AVG-opslagbeperking niet alleen te handhaven, maar ook aantoonbaar te mak

Installation

composer require ginkelsoft-development/laravel-data-retention

De vraag die je liever vooraf had beantwoord

Op enig moment, meestal vlak voor een audit of na een vraag van een klant, krijg je hem op je bord: kun je aantonen dat persoonsgegevens na de afgesproken bewaartermijn ook echt verdwenen zijn? De AVG stelt deze eis in artikel vijf, lid één, sub e: de opslagbeperking. Je mag persoonsgegevens niet langer bewaren dan noodzakelijk voor het doel waarvoor je ze hebt verzameld.

In theorie is dit een rechte lijn. In de praktijk loopt elk Laravel-project dat ik ken vroeg of laat tegen hetzelfde aan. Ergens draait een artisan-command die data opruimt, of een teamlid voert het handmatig uit, en niemand weet honderd procent zeker dat het ook echt gebeurd is. Komt de toezichthouder dat bewijs vragen, dan sta je met lege handen.


Waarom een cronjob niet genoeg is

De meeste teams die ik tegenkom hebben wel iets staan. Een schedule entry, een queue job, een paar delete()-statements verspreid over models. Het werkt, totdat het niet meer werkt. Een kolom krijgt een andere naam, een ontwikkelaar voegt een nieuwe categorie toe en vergeet het beleid, of de cron faalt drie nachten op rij zonder dat iemand het ziet.

Het echte probleem zit er nog onder. Zelfs als de opschoning perfect verloopt, heb je geen onveranderlijk spoor dat het ook echt gebeurd is. De AVG vraagt naast handhaving van de opslagbeperking ook om verantwoording, in artikel vijf, lid twee. Dat tweede stuk is wat het lastig maakt: aantonen, niet alleen doen.

Ik liep hier zelf tegenaan in een Laravel zorgproject waar elke wettelijke bewaartermijn zijn eigen verhaal had — vijf jaar, vijftien jaar, twintig jaar. We bouwden steeds opnieuw varianten van hetzelfde, en bij elke audit moest ik handmatig query-resultaten naast scripts leggen om iets te bewijzen. Dat moest eenvoudiger kunnen.


Wat het package doet

Het package ginkelsoft/laravel-data-retention lost twee dingen tegelijk op. Het automatiseert het opschonen volgens een per model gedefinieerd beleid, én het schrijft een audit-log dat fraude-bestendig is. Geen UI, geen platform, geen losse applicatie — alleen een Laravel package dat in een bestaand project past.

Je verklaart per Eloquent-model wat de bewaartermijn is, vanaf welke datumkolom hij loopt, en wat er moet gebeuren als die termijn voorbij is. Een artisan-command sweept dagelijks de modellen langs en past het beleid toe. Voor elke actie verschijnt een regel in een aparte audit-tabel, en die regels vormen samen een hash-keten waarin elke regel afhankelijk is van de vorige.


Een beleid per model

Een eenvoudig geval, audit-logregels die na twee jaar weg mogen:

use Ginkelsoft\DataRetention\Attributes\Retention;
 use Ginkelsoft\DataRetention\Concerns\HasRetention;
 
 #[Retention(period:'2 years', from:'created_at', action:'delete')]
 class AuditEntry extends Model
 {
     use HasRetention;
 }

Voor situaties waar je het record wel wilt houden maar de persoonsgegevens niet — bijvoorbeeld een ex-klant waar je nog wel administratie van mag voeren — kies je voor anonimiseren, met een strategie per veld:

use Ginkelsoft\DataRetention\Concerns\HasRetention;
 
 class Client extends Model
 {
     use HasRetention;
 
     protectedarray$retention= [
         'period'    => '5 years',
         'from'      => 'ended_at',
         'action'    => 'anonymize',
         'anonymize' => [
             'first_name' => 'placeholder',
             'last_name'  => 'placeholder',
             'bsn'        => 'hash',
             'phone'      => 'null',
         ],
     ];
 }

De ingebouwde strategieën zijn null (waarde leegmaken), hash (één-richtings-SHA-256, contextueel per veld) en placeholder (vervangen door een vaste tekst). Voor maatwerk geef je een closure mee: dan bepaal je per veld zelf hoe een waarde wordt geanonimiseerd.


Het commando

Inplannen is een kwestie van één regel in de scheduler:

$schedule->command('retention:run')->dailyAt('02:00');

De eerste keer dat je het draait, kun je vooraf zien wat er zou gebeuren zonder iets te veranderen:

php artisan retention:run --dry-run

Daarmee zie je per model hoeveel records onder het beleid zouden vallen. Pas als dat klopt, draai je het zonder --dry-run. Idempotent: een tweede run op dezelfde dag vindt niets meer te doen en schrijft ook geen extra regels in het audit-log.


Het audit-log als bewijs

Dit is waar de meeste opruim-scripts ophouden. Elk record dat wordt verwijderd of geanonimiseerd, krijgt een regel in retention_log met daarin: welk model het was, welk primary key, welke actie, welk beleid, wanneer de termijn verliep en wanneer de actie is uitgevoerd.

Wat het log onveranderlijk maakt, is de hash-keten. Elke regel bevat een SHA-256 die wordt berekend over de eigen inhoud, de hash van de vorige regel én een geheim uit je .env. Wijzig je achteraf één veld in één regel, dan klopt vanaf dat moment geen enkele volgende regel meer. Verwijder je een regel, dan breekt de keten op die plek. Voeg je een regel toe zonder het geheim te kennen, dan krijg je de keten ook niet rond.

Verifiëren doe je met één aanroep:

use Ginkelsoft\DataRetention\Support\HashChain;
 use Illuminate\Support\Facades\DB;
 
 $entries=DB::table('retention_log')->orderBy('id')->get()
     ->map(fn ($row) => (array)$row)->all();
 
 $intact= HashChain::verify($entries, config('data-retention.log_secret'));

Belangrijk: het audit-log zelf bevat geen persoonsgegevens. Alleen het modeltype, het primary key en de policy-metadata. De originele veldwaarden gaan met de actie verloren, en dat is precies wat de opslagbeperking eist.


Wanneer dit package past, en wanneer niet

Het past goed in projecten waar de bewaartermijnen vooral op tabel- of modelniveau te beschrijven zijn. Een audit-log na twee jaar weg, een klantdossier vijf jaar na het einde anonimiseren, sessietabellen na zes maanden opruimen — dat soort patroon dekt het ruim.

Het past minder goed in twee situaties. De eerste is wanneer de bewaartermijn afhangt van een regel die per record verschilt, bijvoorbeeld een termijn die mede bepaald wordt door de status van een gerelateerd record in een ander aggregaat. Dat kan met een closure, maar als de regel te zwaar wordt is een eigen actie helderder dan deze laag.

De tweede is wanneer je verwacht dat het verwijderen van een hoofdrecord automatisch ook gerelateerde records meeneemt. Het package raakt bewust alleen het model waarvoor het beleid is ingesteld. Foreign-key-cascades regel je via je database of via expliciete policies op de gerelateerde modellen. Dat is een ontwerpkeuze, geen omissie: stille cascades verbergen vaak juist wat je wilt aantonen.

Verder is het goed om te weten dat soft-deleted records standaard wél meetellen voor retentie en bij een verlopen termijn echt verdwijnen via een force-delete. De gedachte daarachter: een soft-deleted record bevat nog steeds persoonsgegevens, en de opslagbeperking maakt geen onderscheid tussen zichtbaar en onzichtbaar opgeslagen.


Open source, MIT, vrij te gebruiken

Het package is open source onder de MIT-licentie en vrij beschikbaar via GitHub en Packagist. De ondersteunde PHP- en Laravel-versies en de testmatrix vind je in het feiten-kader hieronder. De testsuite draait elke valide combinatie in CI, met PHPStan op niveau max en Pint voor de code style.

Installeren gaat zoals je verwacht:

composer require ginkelsoft/laravel-data-retention
 php artisan vendor:publish --tag=data-retention-config
 php artisan vendor:publish --tag=data-retention-migrations
 php artisan migrate

En in .env één geheim toevoegen voor de hash-keten:

DATA_RETENTION_LOG_SECRET="$(openssl rand -base64 32)"


Onderdeel van een grotere AVG-familie

Dit package is het eerste deel van wat ik bedoel als de GinkelSoft AVG-familie voor Laravel. Het regelt de opslagbeperking. Aanverwante stukken zoals toestemming vastleggen, inzageverzoeken afhandelen, het recht op vergetelheid bewijsbaar uitvoeren, en datalekken registreren komen elk in een eigen package, zodat je alleen pakt wat je nodig hebt. Wie nu deze module gebruikt, krijgt straks dezelfde audit-log-structuur en hetzelfde config-patroon terug in de zustermodules. Zodra die uitkomen vind je ze terug op de overzichtspagina met packages.

Wil je hulp bij het inpassen in een bestaand project, of de bewaartermijnen in jouw situatie eens samen uitwerken? Dat valt onder Laravel onderhoud en maatwerk software. Voor een complete Laravel projectovername kijk ik ook graag mee naar bewaartermijnen als onderdeel van een grotere herijking. Daar denk ik graag in mee — neem contact op als je twijfelt of dit aansluit op jouw situatie.