# Backup, restore and migration

Copy a stopped store, verify the restored copy and distinguish chunking migration from opening a current store.

A safe filesystem copy is made while every writer to the directory is stopped. Copy the whole data directory and every external pin source together. Open the restored copy at a different path and prove its records before changing the original.

## Before you begin

Know the data directory, service configuration, store name, socket and any external reference directories or snapshot files. Keep the binary version that last opened the store. Retain the configuration needed to interpret its paths. Credential files are separate secrets; a store copy is not a credential backup.

Haematite has no CLI `backup` or `restore` subcommand. The permission schema contains backup actions, but those names do not create an online backup API. A collection export or `graph.export` omits other records, branches, history and store metadata.

## Make the copy


1. Stop the service and every other writer using this data directory. A closed client connection is not a stopped service. Wait for the process you own to exit and release the writer lock. Run `haematite history-check <dir>` for this data directory. It must not refuse with `history_check_store_in_use`; resolve any other refusal before proceeding. Keep other writers stopped throughout the copy.

2. Keep direct tree, disk-store, WAL and branch-layer tools away from the directory. Those lower layers can bypass the database writer lock. Do not copy while one is changing files.

3. Copy the whole directory to a new destination. The destination must not exist yet. If it exists, cp places the source directory inside it. Include configuration, branch references, history, snapshot registries, shard WALs, indexes, node files and any migration state. Do not select only files that appear to contain documents.

**Terminal, with every writer stopped**

```bash
cp -a /var/lib/haematite/ward /var/backups/haematite/ward-copy
```


4. Copy every external pin source and record its restored path. Keep this metadata with the same stopped snapshot. A later vacuum must be told about those restored pins.

5. Preserve the original backup. Work on another copy when testing an upgrade or migration.


The `cp -a` form was exercised with both endpoints redirected into the scratch directory and the destination absent. The runs, process exit, lock checks and query output are recorded in `_data/verification-backup-restore.json`. It is a stopped-copy procedure, not a claim that a live recursive copy is consistent or that a particular filesystem snapshot facility is supported.

## Restore and verify

Write a service configuration for the copied data directory. Give the test service a distinct name and socket, with a private socket parent. Do not reuse the original service's named runtime endpoint. Start without `--create`.

List branches and collections. Read known records and retained moments. Run representative queries and inspect both their output and exit codes. Compare the expected record identities and values, not just the fact that the service listened.

The scratch restore reopened the copied store without `--create`. A query through the installed worker returned `p0`, Ada in ward 4B, and `p1`, Bo in ward 4B, with exit 0. Both rows and the copy command are recorded in `_data/verification-backup-restore.json`. `history-check` can add a legacy-file comparison, but its zero-file result is not a whole-store integrity proof. An observer read of one key proves only that key.

Opening for writing can perform a supported storage-format upgrade, rewrite older branch records and activate reclaim. An operator check that opens a writer is not a read-only forensic inspection. Preserve an untouched copy before the first writable open with a new binary. Do not assume an older binary can reopen the changed directory.

## Vacuum before chunking migration

On an eligible old store with much unreachable data, run [Storage and vacuum](/haematite/operations/storage-and-vacuum/) before migrating so the migration need not rebuild garbage. Stop every writer for dry run and sweep. Supply external references and snapshots with the same options on both commands.

Where no complete metadata manifest exists, the dry run and sweep refuse unless the operator explicitly attests metadata completeness. The attestation means the canonical paths plus supplied external paths are every pin source and were written under the supported durability contract. It is not a repair flag.

Policy blockers are checked before the first deletion. A filesystem failure during deletion can leave partial progress. Keep the report, investigate its named path and rerun a dry run after the cause is fixed. A dry run and a later sweep each make their own plan, so their deletion sets agree only if the store and pin metadata have not changed.

## What migrate-chunking changes

`migrate-chunking` is the older whole-directory conversion between count-based chunking and byte-aware chunking. It is not the general upgrade command for the current store format. Its implementation checks the format and refuses unsupported stores before changing files.

**Terminal, on the stopped working copy**

```bash
haematite migrate-chunking /var/lib/haematite/ward --migrate --json
```


The command requires exactly one of `--migrate` and `--abort`. Forward migration rebuilds the affected trees and records a durable migration fence. A crash leaves the directory fenced. Rerunning follows the persisted direction and resumes; an explicit abort can reverse a forward run.

**Terminal, on the stopped working copy**

```bash
haematite migrate-chunking /var/lib/haematite/ward --abort --json
```


Abort is a durable reverse conversion, not deletion of a marker. If an abort direction was already recorded, even a subsequent `--migrate` follows that persisted abort. Never clear the fence or rename partial migration files by hand.

The preflight refuses an unreadable WAL, a refused WAL tail or replayed mutations after a committed root that would otherwise be lost when replacing the WAL. Resolve those conditions through the supported writer recovery on a preserved working copy. Do not trim a WAL to make preflight pass.

> Current stores
> The installed binary refused both directions on scratch format 3 stores with: chunking migration and abort refuse store format 3; no files were changed. A copied released fixture also had format 3 and received the same refusal. No successful count-based conversion, interrupted resume or abort recovery was verified in this documentation run.


## Never change a live writer's files

Do not run migration or vacuum beside a writer. Do not remove `writer.lock` to force access; the operating system lock is the authority and the file can remain after a clean stop. Do not overwrite a live destination with a backup, remove its WAL, edit its stored configuration or replace branch references independently.

`stats` can report beside a writer and marks that moving view. `observe-get` can read a committed key beside one. Neither grants permission to copy, sweep or migrate the files concurrently. A refusal is a reason to find the owning process or damaged input before acting.
