Skip to content

What is backed up, and what is not

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: Back up and restore a Platform site.

The model behind Restore the estate: what a second copy of this estate contains, how often it is taken, what it provably does not contain, and how anybody knows it is real.

Read at nerthus.core.infra.common 7cb02cb and its schema/copy.yml (2026-08-28).

What is copied

Sixteen repositories - every one the estate has - as git bundle --all, uploaded to Cloudflare R2. One entry per repository in schema/copy.yml, by path, and the exclusion list is empty.

The first full copy was 2026-08-27: 16 bundles, 270.5 MB, including the lore corpus and the moderation ledger with its encrypted evidence blobs. Every object was read back with a HEAD request and its size compared against the local file before it was counted.

--all and not HEAD, because a bundle of the default branch is not a copy of a repository. The nerthus.core bundle carries 166 refs.

A bundle is used rather than a tarball for one property: git clone <name>.bundle works directly, which is the property that matters at three in the morning.

How often

Nightly. The job is estate:copy in Nerthus.Core.Infra.Common, and it runs only from a GitLab pipeline schedule carrying the right schedule variable - never on a push.

The cadence itself is not written on this page on purpose. It lives in a GitLab pipeline schedule object rather than in a committed file, so nothing in this repository's pipeline can hold this page to it, and a cron literal that no instrument checks is the failure this site has already had five times over. Read it where it is set: the project's Pipeline schedules page, the entry named for estate:copy. Its last run and its verdict are on the same screen.

A schedule rather than a cron on somebody's machine is the estate's standing rule: a schedule is visible, has a history, and turns red.

What is not copied, by name

A mirror carries commits and refs. It does not carry the things GitLab keeps beside a repository, and these were counted across all 16 projects rather than assumed:

Not in the copy Count
CI/CD variables 54, across 9 projects
deploy keys 17, across 7 projects
deploy tokens 3
pipeline schedules 5
issues and merge requests history only, and only through a project export
the container registry a different subject, and a different runbook

The project export would capture schedules, issues and merge requests - and still not the values of the variables. Those live in the vault, which is itself one of the sixteen repositories, so their recovery path is the life of a credential and not this page.

Tunnel tokens are an accepted loss, and that is measured rather than assumed. A tunnel recreated with the same name and the same secret gets a new id, and the id is inside the token - so every secrets/*.tunnel.age in the fleet repository is dead the moment its tunnel is deleted. Tunnel recovery is a fleet:provision run rather than a restore. The ingress rules in the repository are the durable half.

What makes it a backup rather than an upload

It has been restored from. On 2026-08-26 one bundle was downloaded out of R2 and cloned: restored HEAD equal to the source, tree object 6826e2c8… equal to the source, 388 tracked files on each side, and git diff between the two trees reporting 0 paths.

A matching tree hash is byte-identical tracked content, which is a stronger statement than any file-by-file walk. That is why the comparison is made that way.

A comparison that looks like failure and is not

diff -rq between a restored clone and the source working tree reports .env, .claude, .wrangler and build output as missing. None of them was ever in git. Compare tracked content at HEAD; a working-tree diff reads as "the restore is incomplete" when nothing is missing at all.

What it costs

The copy runs on the self-hosted runner, so it spends no shared CI minutes. R2 holds 270.5 MB per full copy at the layout <yyyy-MM-dd>/<repository>.bundle, and nothing prunes old days today.

Two dates, two questions, and one of them has never happened

Nerthus.Core.Infra.Common/restorability.json publishes exactly two fields:

  • lastSuccessfulCopy - when a copy last succeeded;
  • lastSuccessfulRehearsal - when somebody last restored from one as a rehearsal.

They have one writer each, enforced, and they are timestamps rather than ages. null means never and not unknown-and-probably-fine, which is why a plausible default was refused: it would have converted an absence into an assertion with authority attached.

The committed file is the seed and carries no dates, and it says so in its own thisFileIs field. The live values are in the job's artifact. A date hand-written into the seed once made a real copy fail: the file said 2026-08-27T00:05:00Z while the job ran at 2026-08-26T23:20:52Z, and the guard that refuses a date moving backwards was right to refuse it.

Nothing consumes these two fields yet, and no check asserts they are recent. That is stated here rather than left for a reader to discover, because a published field nobody reads is exactly as good as no field at all.

See also