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¶
- Restore the estate - the procedure, for when gitlab.com does not have the data
- Roll the fleet back - a bad image pin, which is a different incident
- The life of a credential - the half a bundle does not carry
- The corpora a host holds - which repositories the estate has at all