---
title: "HitKeep Backup and Restore Guide | HitKeep"
description: "Back up and restore HitKeep safely across single-tenant and multiteam installs, including the SQLite control plane and tenant DuckDB data plane."
canonical: "https://hitkeep.com/guides/data/backups-and-restore/"
---

# HitKeep Backup and Restore Guide

HitKeep keeps its operational story simple, but every 2.13 installation has an explicit storage boundary:

- `HITKEEP_DB_PATH` is the SQLite control-plane database
- every tenant, including the default tenant, stores analytics in its own DuckDB file under one `data-path`

That means your backup strategy must follow the real storage boundary, not an older “copy one `hitkeep.db` file” assumption.

## The Backup Boundary

### Single-tenant or default-tenant only

If you only use the default tenant, your live data is:

- the control-plane database at `HITKEEP_DB_PATH` (commonly `{data-path}/hitkeep.db`)
- the default tenant analytics database at `{data-path}/tenants/{default_team_id}/hitkeep.db`
- your archive directory if retention archiving is enabled

### Multiteam installs

Once you use non-default teams, the live data footprint becomes:

- the control-plane database at `HITKEEP_DB_PATH`
- one analytics database per tenant at `{data-path}/tenants/{team_id}/hitkeep.db`, including the default tenant
- your archive directory if retention archiving is enabled

That is why the safe rule is:

> Back up the whole `data-path` tree, not only `hitkeep.db`, even when you use only the default tenant.

## Built-in Backups

HitKeep creates a compact, consistent SQLite control snapshot with `VACUUM INTO` and exports each tenant catalog with DuckDB.

```
export HITKEEP_DATA_PATH=/var/lib/hitkeep/data
export HITKEEP_BACKUP_PATH=/var/lib/hitkeep/backups
export HITKEEP_BACKUP_INTERVAL=60
export HITKEEP_BACKUP_RETENTION=24

./hitkeep
```

Built-in backups include:

- `shared/{timestamp}/control.db.zst`, a versioned SQLite manifest, and `_COMPLETE` written last
- each tenant snapshot, including the default tenant, under `tenants/{team_id}/{timestamp}/`

Use this when you want consistent application-level snapshots without relying on filesystem-level tooling.

HitKeep passively checkpoints SQLite before the control snapshot and runs a required catalog-scoped DuckDB checkpoint before each tenant export. A failed checkpoint or validation fails the backup instead of publishing a completion marker.

One timestamp is successful only when the control database and every active tenant database export successfully; `_COMPLETE` is the cycle-wide commit marker. A tenant failure marks the whole run as failed even if other tenant exports completed. Failed or interrupted runs can leave uncommitted tenant export directories or S3 objects, but restore ignores local control snapshots without `_COMPLETE` and rejects an explicitly selected incomplete timestamp. Expire abandoned S3 prefixes through your normal object-storage lifecycle policy.

Instance operators can inspect the configured backup path, interval, retention, next backup, last backup, and recent failures from **Administration → System Status → Operations**.

## Restore Semantics

Restore is an **offline** operation:

```
./hitkeep recover restore-backup \
  -from /var/lib/hitkeep/backups \
  -snapshot 2026-03-08T120000Z \
  -db /var/lib/hitkeep/data/hitkeep.db \
  -data-path /var/lib/hitkeep/data \
  -yes
```

For S3-backed snapshots:

```
./hitkeep recover restore-backup \
  -from s3://my-bucket/hitkeep/backups \
  -snapshot 2026-03-08T120000Z \
  -yes
```

The restore flow is WAL-safe and engine-aware:

1. Existing database files are moved aside as `.pre-restore.{timestamp}` safety copies.
2. A SQLite control snapshot is low-memory decompressed into a temporary file, checked against its size and SHA-256 manifest, and validated with `quick_check` plus `foreign_key_check`.
3. Each tenant snapshot is imported into a temporary DuckDB file, checkpointed, and closed.
4. HitKeep refuses incomplete snapshots and tenant restores that still depend on a `.wal`.
5. Each validated file is renamed into place individually while the previous file remains as a `.pre-restore.{timestamp}` safety copy. If any catalog fails, keep HitKeep stopped, correct the backup set, and rerun restore before startup. Legacy DuckDB shared snapshots are still accepted and are converted offline on the next 2.13 startup.

That means `hitkeep recover restore-backup` itself should not leave your restored database dependent on replaying a leftover WAL.

The `.pre-restore.{timestamp}` database and WAL safety copies are retained after a successful restore. Validate the restored instance first, then remove those copies according to your storage and data-retention policy; HitKeep does not rotate them automatically.

Automatic-recovery bundles under `HITKEEP_DB_RECOVERY_PATH` are different from backup snapshots. They preserve the exact pre-repair database and WAL for rollback or forensic work, are retained locally, and are not rotated by the backup retention setting.

## Local vs External Backup Tooling

Built-in backups are the easiest supported option, but external tooling is still valid.

### Recommended external strategy

Stop HitKeep before copying live DuckDB files, or use storage-level snapshots that quiesce the volume consistently. Do not copy an active database and WAL as unrelated files.

```
# HitKeep is stopped for this copy.
# Copy the full live data tree
rsync -az /var/lib/hitkeep/data/ backup-host:/backups/hitkeep/data/

# Copy retention archives if you use them
rsync -az /var/lib/hitkeep/archive/ backup-host:/backups/hitkeep/archive/
```

Or with object storage:

```
# HitKeep is stopped for this copy.
rclone sync /var/lib/hitkeep/data/ remote:my-bucket/hitkeep/data/
rclone sync /var/lib/hitkeep/archive/ remote:my-bucket/hitkeep/archive/
```

For every 2.13 installation, **do not** back up only:

```
cp /var/lib/hitkeep/data/hitkeep.db /backups/
```

That copies only the shared control plane and misses tenant-local analytics databases.

## 2.13 Migration and Downgrades

The first 2.13 startup migrates default-tenant analytics out of `hitkeep.db`, drains retained non-default compatibility rows into their tenant catalogs, rewrites the control file, and converts it to SQLite. It may require one clean restart if automatic DuckDB recovery ran first. The former compact DuckDB control file remains as `<db-path>.pre-sqlite-2.13.0` for migration recovery evidence; it is not a 2.12 downgrade database. Downgrading requires restoring the complete pre-upgrade control-and-tenant backup.

## After a Restore

After `recover restore-backup` finishes:

1. Start HitKeep normally.
2. Confirm login works.
3. Open the dashboard for a site in the default tenant.
4. Open at least one site from a non-default team if you use teams.
5. Check that goals, funnels, ecommerce, and team-specific analytics still render.

The next normal startup may create a new, valid DuckDB `.wal` during runtime. That is expected. The thing to avoid is a restore that only works if an old or partial WAL is replayed.

## Recovering Without a Backup

If the default tenant’s analytics database at `{data-path}/tenants/{default_tenant_id}/hitkeep.db` is lost and no backup exists — for example because the data path was not on persistent storage — HitKeep refuses to start and names the missing file. Two offline commands get you running again. Stop HitKeep before using either one.

Rebuild the missing database as an empty, schema-migrated file:

```
./hitkeep recover rebuild-default-tenant
```

The command requires explicit confirmation because analytics history is not restored. It refuses to overwrite a database that still opens, and it sets an unreadable leftover file aside instead of deleting it.

If retention archiving was active, your aged-out analytics still exist as Parquet exports under the archive path. Import them back after the rebuild:

```
./hitkeep recover import-archives
```

The import is idempotent: rows that already exist are kept once, and rows that reference a deleted site, goal, or funnel are skipped rather than imported inconsistently. Data newer than your last archive run cannot be recovered this way — only a real backup holds it.

Both commands read `HITKEEP_DB_PATH`, `HITKEEP_DATA_PATH`, and `HITKEEP_ARCHIVE_PATH` from the environment and accept explicit `-db`, `-data-path`, and `-archive-path` flags.

## Related

- [S3 Backups](https://hitkeep.com/guides/data/s3-backups/)
- [Disaster Recovery](https://hitkeep.com/guides/data/disaster-recovery/)
- [Data Retention and Archiving](https://hitkeep.com/guides/data/retention/)
- [System Status and Settings](https://hitkeep.com/guides/admin/system-administration/)
- [Configuration Reference](https://hitkeep.com/reference/configuration/)

[Previous Performance and memory](https://hitkeep.com/guides/installation/performance/)[Next S3 backups](https://hitkeep.com/guides/data/s3-backups/)
