Your database is piling up inactive users, canceled orders, access logs from years ago. GDPR requires strict retention limits, but physically deleting data is risky: what if a client asks for restoration, an audit demands history, or a payment is disputed? Soft delete is the technical solution that balances compliance and operability. But without an automatic process for final removal, you risk accumulating ghost data that violates the minimization principle. Here at Meteora Web, we see companies implementing soft delete without a retention plan every day. Result: bloated tables, slow backups, privacy vulnerabilities. In this guide we'll show you how to design a data retention system that balances soft delete and automatic deletion, with working code and GDPR-compliant logic.
Soft Delete vs Hard Delete: When to Use Each
Soft delete marks a record as removed without physically deleting it. Implement it with a deleted_at field (Laravel) or is_deleted, and exclude it from standard queries. The advantage is fast recovery and traceability. The downside? Data stays in the database. Hard delete removes the record irreversibly. It's mandatory when the retention period expires and there are no legal obligations (e.g., invoices).
Which one for each data type
There's no one-size-fits-all. Here's how we handle it in real projects:
Sponsored Protocol
- Personal data (users, contacts): soft delete for 30–90 days (restoration window), then automatic hard delete.
- Orders and invoices: permanent soft delete? No. For fiscal obligations (e.g., 10 years) keep the data, but anonymize personal fields (name, email).
- Access logs: hard delete after 6 months, unless security standards require longer (e.g., PCI-DSS 1 year).
- Sessions and tokens: immediate hard delete upon expiry, no soft delete.
GDPR and Data Retention: Legal Limits
Article 5(1)(e) GDPR requires data to be kept for no longer than necessary. Each purpose has a different duration. As a developer, you must translate these constraints into automated rules. You can't leave the decision to individual operators.
Purpose-Retention Mapping
Before writing a line of code, map every database entity with:
- Processing purpose
- Legal basis
- Retention period
- Possible anonymization requirement
Create an external configuration table (JSON, DB, .env) that can be modified without deployment. Example:
{
"users": {
"purpose": "account management",
"retention_days": 365,
"soft_delete_days": 30,
"action_after_retention": "anonymize"
},
"order_logs": {
"purpose": "transaction history",
"retention_days": 3650,
"soft_delete_days": 0,
"action_after_retention": "hard_delete"
}
}We use this pattern for all our clients. It lets you adapt policies without touching code.
Sponsored Protocol
Practical Implementation: Soft Delete with Laravel and SoftDeletes
Laravel provides the SoftDeletes trait. Use it for the first deletion level.
Model with SoftDeletes
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
}When you call $user->delete(), Laravel sets deleted_at with the current timestamp. Normal queries exclude these records (via a global scope). You can retrieve them with User::withTrashed()->find($id) and restore with $user->restore().
Automatic Deletion: Scheduler and Artisan Commands
Soft delete isn't enough. After the retention period (e.g., 30 days soft delete + 365 total retention) you must physically remove the data. Create an Artisan command.
// app/Console/Commands/PurgeSoftDeletedRecords.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\Order;
use Carbon\Carbon;
class PurgeSoftDeletedRecords extends Command
{
protected $signature = 'retention:purge';
protected $description = 'Permanently delete expired soft-deleted records';
public function handle()
{
$config = config('retention'); // mapping table
foreach ($config as $modelClass => $rules) {
$deadline = Carbon::now()->subDays($rules['retention_days'] - $rules['soft_delete_days']);
// Records soft-deleted before the deadline get force-deleted
$query = $modelClass::onlyTrashed()
->where('deleted_at', '<', $deadline);
$count = $query->count();
$query->forceDelete();
$this->info("Deleted {$count} records from {$modelClass}");
}
}
}Register the command in app/Console/Kernel.php and schedule it daily:
Sponsored Protocol
protected function schedule(Schedule $schedule)
{
$schedule->command('retention:purge')
->dailyAt('03:00')
->withoutOverlapping();
}Retention on Non-Laravel Databases (PHP/SQL)
If you're not using Laravel, you can implement the same pattern with raw queries. MySQL example:
-- Add deleted_at column to users table
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;
-- Query to permanently delete soft-deleted records older than 30 days
DELETE FROM users
WHERE deleted_at IS NOT NULL
AND deleted_at < NOW() - INTERVAL 30 DAY;
-- Soft delete (marking)
UPDATE users SET deleted_at = NOW() WHERE id = ?;Automate with a cron job:
Sponsored Protocol
# crontab -e
0 3 * * * php /path/to/purge.phpThe purge.php file reads the configuration and runs conditional DELETE statements.
Anonymization vs Deletion
In some cases (e.g., invoices) you cannot delete the record, but you can anonymize personal data. An email becomes redacted-{id}@domain.internal, name becomes Deleted User. Implement a separate anonymization command.
// Anonymization command
public function handle()
{
$users = User::where('deleted_at', '<', Carbon::now()->subDays(60))->get();
foreach ($users as $user) {
$user->email = 'anon-'.$user->id.'@example.com';
$user->name = 'Deleted User';
$user->save();
}
}Important: anonymization must be irreversible. Do not store the original email anywhere.
Monitoring and Logging
Automatic retention without audit is dangerous. Log every hard delete or anonymization action in a non-deletable log (e.g., syslog, append-only table). We recommend logging:
- Timestamp of the operation
- Number of affected records
- Date range
- Command or job ID
Also, set an alert if the command hasn't run for more than 48 hours (via external monitoring or internal check). A scheduler failure can lead to GDPR violations.
Sponsored Protocol
Implementation Checklist
- Map all entities with purpose, retention, soft delete, and final action.
- Implement soft delete for data that may need restoration (users, content, partial orders).
- Create automatic commands for hard delete after the soft-delete window.
- Handle anonymization for data that must remain due to accounting obligations.
- Set up the scheduler and monitor execution.
- Test on staging with a sample dataset.
- Document policies and keep them updated in the processing records.
In Summary — What to Do Now
Don't wait for an audit. Today:
- Identify 3 tables in your database that contain personal data.
- Determine for each the retention period and soft-delete window.
- Write a simple command (even SQL via cron) to clean up records beyond the window.
- Add logging for the operation.
- Verify the scheduler is running (on Linux:
systemctl status cron).
If you need a structured approach, check out our GDPR pillar guide for developers, where we cover cookies, consent, and the right to erasure. Data retention is just one piece of the puzzle — but if done wrong, it can cost you dearly. We at Meteora Web see it every day: a wrong hard delete loses customers, an infinite soft delete risks fines. Design carefully, automate rigorously.