PHPackages                             ginkelsoft/laravel-data-retention - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Validation &amp; Sanitization](/categories/validation)
4. /
5. ginkelsoft/laravel-data-retention

ActiveLibrary[Validation &amp; Sanitization](/categories/validation)

ginkelsoft/laravel-data-retention
=================================

A Laravel package that enforces GDPR storage-limitation rules per Eloquent model by deleting or anonymizing expired records and writing a tamper-evident audit log.

v1.0.0(1mo ago)001MITPHPPHP ^8.2CI failing

Since May 26Pushed 1mo agoCompare

[ Source](https://github.com/ginkelsoft-development/laravel-data-retention)[ Packagist](https://packagist.org/packages/ginkelsoft/laravel-data-retention)[ RSS](/packages/ginkelsoft-laravel-data-retention/feed)WikiDiscussions development Synced 1w ago

READMEChangelog (1)Dependencies (9)Versions (3)Used By (1)

Ginkelsoft Laravel Data Retention
=================================

[](#ginkelsoft-laravel-data-retention)

[![Tests](https://github.com/ginkelsoft-development/laravel-data-retention/actions/workflows/tests.yml/badge.svg?branch=development)](https://github.com/ginkelsoft-development/laravel-data-retention/actions/workflows/tests.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/1ec1f2e2c18f4a763ba4f369ed8f94ba8b4d9a85b03e06d740ea081a5cfc7355/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f67696e6b656c736f66742f6c61726176656c2d646174612d726574656e74696f6e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ginkelsoft/laravel-data-retention)[![License](https://camo.githubusercontent.com/6c711032aff1ca0eb6b211aa6cb3649ce7fd64a7714e1181d4bb457f9680e7cf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e7376673f7374796c653d666c61742d737175617265)](LICENSE)[![Laravel](https://camo.githubusercontent.com/255077649ac4ed79e446c4d860d1c53c9764b8ff855456caeb401faf7d250205/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31302d2d31332d627269676874677265656e3f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c)](https://laravel.com)[![PHP](https://camo.githubusercontent.com/482d9c7691869be159b0bd042060a220a12a89396d63fa223c22b74affaf42c4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532302d2d253230382e352d626c75653f7374796c653d666c61742d737175617265266c6f676f3d706870)](https://php.net)[![PHPStan](https://camo.githubusercontent.com/fa63e0381a93ba9755a46ec197198ef973137dca1643836d06b3d6263c9aa7c8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c2532306d61782d627269676874677265656e3f7374796c653d666c61742d737175617265)](phpstan.neon.dist)

Overview
--------

[](#overview)

The GDPR — and its Dutch implementation, the AVG — requires that personal data be kept for no longer than is necessary for the purposes for which it is processed (art. 5(1)(e), the **storage-limitation principle**). Knowing this rule is one thing; **proving** that it is being applied consistently across an Eloquent codebase is another.

**Laravel Data Retention** lets you declare a retention policy on every Eloquent model, sweeps expired records on a schedule, and writes a **tamper-evident audit log** that lets you demonstrate, after the fact, that the data really was retired — when, how, and under which policy.

This is the storage-limitation member of the GinkelSoft compliance family. The other AVG controls (right to be forgotten, subject access, consent, breach registry) live in sibling packages — install the hub to get the whole family in one go. See **The family** below.

> **Upgrading from v1.x?** The v1.x release of this package bundled five controls in one. v2.x reduces it to storage limitation only; the other four are now separate packages. Hash chains in existing `retention_log` tables remain byte-identical and continue to verify. See [UPGRADE.md](https://github.com/ginkelsoft-development/laravel-compliance-core/blob/development/UPGRADE.md)in the core repo for the migration steps.

The family
----------

[](#the-family)

PackageGDPR Article(s)Role[`laravel-compliance-core`](https://github.com/ginkelsoft-development/laravel-compliance-core)art. 5(2)Shared primitives (hash chain, strategies, subject hash)**`laravel-data-retention`****art. 5(1)(e)****Storage limitation — this package**[`laravel-data-right-to-be-forgotten`](https://github.com/ginkelsoft-development/laravel-data-right-to-be-forgotten)art. 17Subject-driven erasure[`laravel-data-subject-access`](https://github.com/ginkelsoft-development/laravel-data-subject-access)art. 15 + 20Read-only subject export[`laravel-data-consent`](https://github.com/ginkelsoft-development/laravel-data-consent)art. 6(1)(a) + 7Consent registry[`laravel-data-breach-registry`](https://github.com/ginkelsoft-development/laravel-data-breach-registry)art. 33 + 34Personal-data breach register[`laravel-compliance-hub`](https://github.com/ginkelsoft-development/laravel-compliance-hub)art. 5(2)Umbrella: installs the whole family, verifies every chainHow it works
------------

[](#how-it-works)

### 1. Declare a policy on the model

[](#1-declare-a-policy-on-the-model)

**Attribute form** — best for "just delete after N years":

```
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;
}
```

**Property form** — required for `anonymize`, because each field gets its own strategy:

```
use Ginkelsoft\DataRetention\Concerns\HasRetention;

class Client extends Model
{
    use HasRetention;

    protected array $retention = [
        'period'    => '5 years',
        'from'      => 'ended_at',
        'action'    => 'anonymize',
        'anonymize' => [
            'first_name' => 'placeholder',  // [REDACTED]
            'last_name'  => 'placeholder',
            'bsn'        => 'hash',         // one-way SHA-256, contextual
            'phone'      => 'null',         // overwrite with NULL
            'notes'      => fn ($value, $field, $model) => 'anon-' . $model->id,
        ],
    ];
}
```

### 2. Register the models

[](#2-register-the-models)

`config/data-retention.php`:

```
return [
    'models' => [
        \App\Models\AuditEntry::class,
        \App\Models\Client::class,
    ],
    'include_soft_deleted' => true,
    'chunk_size' => 500,
];
```

Only listed models are touched. Forgetting to list a model is the safe failure mode.

### 3. Schedule the sweep

[](#3-schedule-the-sweep)

```
// app/Console/Kernel.php
$schedule->command('retention:run')->dailyAt('02:00');
```

Or run it manually:

```
php artisan retention:run --dry-run     # safe preview
php artisan retention:run               # actually retire data
php artisan retention:run --model="App\\Models\\Client"
php artisan retention:run --chunk=1000
```

### 4. Read the audit log

[](#4-read-the-audit-log)

Every action produces one row in the `retention_log` table:

model\_typemodel\_idactionretention\_periodretention\_fieldexpired\_atperformed\_atprevious\_hashhashApp\\Models\\Client01HXYZ...anonymized5 yearsended\_at2020-05-26 00:00:002026-05-26 02:00:01(prev sha256)sha256…Verify the entire chain at any time, using the shared core `HashChain`:

```
use Ginkelsoft\ComplianceCore\Config\LogSecret;
use Ginkelsoft\ComplianceCore\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, LogSecret::value());
```

If the chain ever fails to verify, **somebody touched the log table**. That is exactly what auditors want to be able to detect. (Or run `php artisan compliance:verify` from the [hub](https://github.com/ginkelsoft-development/laravel-compliance-hub) to verify every chain in the family in one go.)

Security model
--------------

[](#security-model)

ThreatMitigationRetroactive edit of a log rowEvery row's hash depends on the previous row's hash + a shared secret. Editing a row invalidates every later row.Forged log row inserted by an attacker without the `log_secret`The forged row cannot produce a hash that chains with both its neighbors.Application code accidentally mutating the log`RetentionLogEntry` throws on `update()` / `delete()`. Bypassing it via raw queries still breaks the chain.Leaking PII via the log itselfOnly class + primary key + policy metadata are logged. No field values, ever.Soft-deleted rows hiding personal data forever`retention:run` calls `forceDelete()` on soft-deleted models so the storage-limitation principle is actually satisfied.Operator confidence before first run`--dry-run` reports every action that would occur, writes nothing.The shared `compliance.log_secret` (with BC fallback to `data-retention.log_secret`) plays the role of a HMAC key: it lives in `.env`, not the database, so an attacker with read/write access to the DB cannot forge a consistent chain unless they also exfiltrate the secret.

Compliance notes
----------------

[](#compliance-notes)

- **GDPR art. 5(1)(e) / AVG art. 5 lid 1 sub e** — Storage limitation. This package gives you a documented, automated mechanism to retire data and a tamper-evident record that demonstrates compliance to the supervisory authority.
- **GDPR art. 5(2)** — Accountability. The hash chain (provided by core) is the evidence.
- **ISO 27001:2022 A.5.34 / A.8.10** — Information deletion. The audit log produces the "records of erasure" that this control requires.
- **NEN 7510 / NEN 7513** (Dutch healthcare) — Append-only logging of data lifecycle events is compatible with NEN 7513 requirements; the log itself stores no patient data.

This package is **not legal advice**. Retention periods must be set by your DPO based on your processing purposes.

Installation
------------

[](#installation)

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

Then add a secret to `.env`:

```
COMPLIANCE_LOG_SECRET="$(openssl rand -base64 32)"
```

Existing installations upgrading from v1.x can keep their `DATA_RETENTION_LOG_SECRET` — core's `LogSecret` helper reads it as a fallback so existing hash chains keep verifying after the upgrade.

Configuration reference
-----------------------

[](#configuration-reference)

OptionDefaultDescription`models``[]`Classes processed by `retention:run`. Anything not listed is never touched.`include_soft_deleted``true`Whether soft-deleted rows also count for retention. Recommended `true` (they still hold PII).`chunk_size``500`Records per chunk when iterating large tables.The signing secret and anonymize placeholders are in the shared [`compliance` config](https://github.com/ginkelsoft-development/laravel-compliance-core)provided by `ginkelsoft/laravel-compliance-core`.

Anonymize strategies
--------------------

[](#anonymize-strategies)

The three built-in strategies live in [`laravel-compliance-core`](https://github.com/ginkelsoft-development/laravel-compliance-core)and are shared with `laravel-data-right-to-be-forgotten`:

Strategy idClassOutput`'null'``Ginkelsoft\ComplianceCore\Strategies\NullStrategy`Sets the field to `null`.`'hash'``Ginkelsoft\ComplianceCore\Strategies\HashStrategy`SHA-256 of `{model}`'placeholder'``Ginkelsoft\ComplianceCore\Strategies\PlaceholderStrategy`The configured placeholder string (`[REDACTED]` by default).`Closure`(resolved inline)`function (mixed $value, string $field, Model $model): mixed`Implement `Ginkelsoft\ComplianceCore\Contracts\AnonymizeStrategy` if you need a reusable custom strategy.

Trying it out (demo seeder)
---------------------------

[](#trying-it-out-demo-seeder)

```
php artisan db:seed --class="Ginkelsoft\\DataRetention\\Database\\Seeders\\RetentionDemoSeeder"
php artisan retention:run --dry-run
php artisan retention:run
```

After the run:

- 10 of the 20 audit entries are deleted (the expired ones).
- 5 of the 15 clients are anonymized in place.
- 15 audit-log rows are written, chained, and verifiable.

Framework compatibility
-----------------------

[](#framework-compatibility)

Laravel VersionSupported PHP Versions**10.x**8.2 – 8.3**11.x**8.2 – 8.4**12.x**8.3 – 8.5**13.x**8.3 – 8.5### Supported databases

[](#supported-databases)

Database-agnostic — anything Eloquent supports works. Tested on MySQL, MariaDB, PostgreSQL, SQLite (used in CI), SQL Server.

Gotchas
-------

[](#gotchas)

- **Relations are not cascaded.** This package only touches the model whose retention policy expired. Give related models their own policy or use database-level `ON DELETE CASCADE`.
- **NULL in the `from` field never expires.** A `Client` with `ended_at = null` is treated as still active. Mis-typed or never-populated columns will quietly skip retention.
- **Soft-deleted rows are force-deleted on expiry.** Retention will eventually empty the trash. Set `include_soft_deleted` to `false` only if you have a separate cleanup process for trashed rows.
- **The audit log grows forever.** Plan to archive `retention_log` rows older than your statutory audit-trail retention (usually 5–7 years in NL) **to cold storage**, not delete them — and verify the hash chain before archiving.
- **Changing `log_secret` invalidates the existing chain.** Rotate it only as part of an explicit audit rotation procedure.
- **`hash` strategy needs a wide column.** The output is 64 hex chars. If a field is `VARCHAR(20)`, the migration to widen it should land before you turn on retention.

Testing
-------

[](#testing)

```
composer install
vendor/bin/pest
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
```

Contributing
------------

[](#contributing)

Pull requests welcome — read [CONTRIBUTING.md](CONTRIBUTING.md) first.

Security
--------

[](#security)

Found a vulnerability? **Do not open a public issue.** See [SECURITY.md](SECURITY.md) for the private reporting channel.

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md).

Reporting bugs
--------------

[](#reporting-bugs)

Found a bug or unexpected behaviour? We want to hear about it.

**Preferred — open a GitHub issue:**

When opening an issue, please include:

1. **Versions** — PHP, Laravel, and the package version (`composer show ginkelsoft/laravel-data-retention`).
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 **** directly with "SECURITY" in the subject line and we will respond privately.

**Not on GitHub?** You can also e-mail **** with the same information.

Contact
-------

[](#contact)

For commercial support, integration questions, or anything that doesn't fit a GitHub issue: **** — .

License
-------

[](#license)

MIT License — see [LICENSE](LICENSE). (c) 2026 Ginkelsoft

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance88

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

59d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/165766253?v=4)[ginkelsoft](/maintainers/ginkelsoft)[@GinkelSoft](https://github.com/GinkelSoft)

---

Top Contributors

[![ginkelsoft-development](https://avatars.githubusercontent.com/u/179240029?v=4)](https://github.com/ginkelsoft-development "ginkelsoft-development (14 commits)")

---

Tags

laravelgdprcomplianceaudit-logdata-retentionanonymizeavgginkelsoftstorage-limitation

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ginkelsoft-laravel-data-retention/health.svg)

```
[![Health](https://phpackages.com/badges/ginkelsoft-laravel-data-retention/health.svg)](https://phpackages.com/packages/ginkelsoft-laravel-data-retention)
```

###  Alternatives

[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k30.2M151](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
