Deleting the primary record and losing the ability to answer "how much traffic did this account generate before it left" are two different outcomes, and conflating them is how support tickets turn into incidents. Archiving keeps the second thing from disappearing with the first.
The fleet's analytics service snapshots a site's aggregated data into a dedicated archive table, from the same flow that responds to a site's deletion, before the live records it summarizes are gone for good:
// app/Models/Archive.php
class Archive extends Model
{
use HasUuids;
protected $fillable = [
'analytics_id',
'url',
'data',
'deleted_at',
];
protected function casts(): array
{
return [
'data' => 'json',
];
}
}
// app/Listeners/SiteDeletedListener.php
class SiteDeletedListener
{
public function handle(SiteDeletedEvent $event): void
{
Archive::create([
'analytics_id' => $event->site->id,
'url' => $event->site->name,
'data' => $event->aggregations->toArray(),
'deleted_at' => now(),
]);
}
}
Archive: a small, deliberately generic table. It doesn't preserve the original schema, it preserves a JSON snapshot of what mattered at the moment of deletion.
SiteDeletedEvent: carries the site and its already-computed aggregations, not raw rows. Archiving the summary, not the source data, is what keeps the archive table cheap forever instead of becoming a second copy of the same growth problem you were trying to solve.
The listener, not the deletion code itself: the archive write is decoupled from whatever triggered the deletion, so anything that fires the event gets the same guarantee without duplicating the archiving logic at every call site.
This is also why an ops tool exists that shows a deleted site's merchant metadata and its analytics archive side by side. Archiving isn't useful if nobody can find what got archived six months later. If you can't look a deleted record back up, you didn't archive it. You just delayed the delete.
Proving what you keep and why
Retention rules and archive tables only do their job if someone can point at them and say "here's why we keep this, and here's proof it's happening." A policy nobody can verify is a policy nobody trusts, including the team that wrote it.
Two things make a policy provable instead of aspirational. First, thresholds live in config, not in a comment or someone's memory: a job that reaps stale, unconverted trials reads its cutoff from config('services.server.management.trial_reap_after_hours') rather than a number buried in a method body. Change the policy, change the config, and every enforcer picks it up the same way. Second, the evidence that a policy ran has to be inspectable after the fact, which is exactly what the deleted-sites ops panel gives an operator: pull up a deleted account and see, concretely, what survived in the archive and what didn't.
Neither is glamorous. Both are the difference between "we have a retention policy" as a sentence in a document and a retention policy you can actually defend when someone asks.
Chapter 11 Summary
Ownership and cost:
- [X] Every table and JSON blob has a named owner and a one-line reason it exists
- [X] "I don't know why we keep this" is treated as a bug, not a shrug
Retention:
- [X] Retention lives on the model (
Prunable,prunable()), not buried in a standalone command - [X] One scheduled
model:prunesweep covers every prunable model, not one command per table
Migrations and backfills:
- [X] Live-table migrations stream with
cursor(), never paginate writes with offset-basedchunk() - [X] Backfill commands ship with
--dry-run, chunked reads, and a skip-if-unchanged check so they're restartable - [X] Additive schema changes check for existence first, so a migration can survive being run twice
Deletion and archiving:
- [X] A deletion request is partitioned by ownership before anything is torn down
- [X] Hard-delete what only that identity gave meaning to; soft-delete or archive what still has value
- [X] Cold data is archived as a deliberate snapshot, decoupled from the deletion trigger itself, and stays inspectable after the fact
Data doesn't manage itself, and pretending it will is how a directory clean-up script becomes your entire data strategy. Decide who owns it, decide how long you keep it, and decide what happens to it on the way out. Do that once, on purpose, and you stop discovering your data policy by accident.