# Backups (https://docs.vitrinka.ai/security/backups)



## The sealed archive [#archive]

`vitrinka backup` takes a consistent `VACUUM INTO` copy of the control database and of every workspace database — reads only, the sources are never mutated — tars them with a manifest, and seals the stream under a key from its own environment: `VITRINKA_KEK`, falling back to `VITRINKA_MASTER_KEY`. The result is one `enc2f` file: framed AES-256-GCM chunks under a fresh wrapped data key ([Encryption](/security/encryption#sealed)). With the [OpenBao key store](/security/encryption#keystore) the server runs without `VITRINKA_KEK`, and without `VITRINKA_MASTER_KEY` once the key store has adopted it, so give the backup run `VITRINKA_MASTER_KEY` in its environment and keep that key with your recovery material — `restore-backup` needs the same one.

```bash
vitrinka backup -data /data -out /backups/vitrinka-2026-08-24.vtbk
vitrinka restore-backup -in /backups/vitrinka-2026-08-24.vtbk -out /restore
```

A stolen archive is ciphertext. Because the sealed columns inside it stay under their workspace keys, a workspace that is later crypto-shredded is unreadable inside every archive taken after the shred ([Retention and deletion](/security/retention#shred)).

## What it excludes [#excludes]

Blobs are deliberately not in the archive. They are already per-workspace ciphertext at rest, so replicating the blob store replicates ciphertext; with `BLOB_BACKEND=fs` back up `DATA_DIR/blobs` alongside, with S3 the bucket is the durable copy.

The licence file is not in `DATA_DIR` either — it lives in the bundle's `./license/` directory ([Operations](/operator/operations#license-location)).

## Restoring [#restore]

`restore-backup` unseals and unpacks into an empty directory only. It never overwrites a live `DATA_DIR`; pointing a stopped server at the restored tree, or copying files over after your own diff, stays a deliberate operator step.

<Callout type="warn">
  Operator safety snapshots go to `VITRINKA_BACKUP_DIR`. A path that resolves inside `DATA_DIR` is refused, so a backup can never be swept up by the thing it protects.
</Callout>

## The compose-level snapshot [#compose]

The simplest self-hosted routine — stop, tar `/data`, start — is in [Operations](/operator/operations#backup). It captures databases and filesystem blobs in one archive but is not sealed by the application; keep it on an encrypted volume.

## Continuous replication [#replica]

The hosted service uses Litestream to replicate its databases to object storage. The deployment config sets a 10-second sync interval, daily snapshots and validation every six hours. Object expiry is controlled separately by the storage policy; these intervals do not guarantee a particular recovery window. Sealed columns remain encrypted in the replica. Database metadata, including identifiers, timestamps and addresses, depends on transport security and bucket access controls. The sealed archive above provides a separate backup method.

## Standby and write fencing [#standby]

For a two-host deployment the server has a standby role and a write fence. `VITRINKA_ROLE=standby` boots a process that answers the health probes and nothing else: `/healthz` is 200, `/readyz` and every other route 503. It opens no database and runs no background work, so a replica being followed into its `DATA_DIR` is never touched; keeping that replica current is the job of the replication you run. Promotion is clearing the role and restarting.

`VITRINKA_FENCE_URL` is the write fence: before its first write at boot a writer fetches that small document and refuses to start unless it names `VITRINKA_FENCE_HOST`. An unreachable fence refuses too, and a standby never consults it. The fence is read at boot, so stop the old writer before the document names the new one. Both are in the [configuration reference](/reference/env#core).
