Skip to content
Start free in Cloud

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.

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

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.

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

Terminal window
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 is an offline operation:

Terminal window
./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:

Terminal window
./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.

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

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.

Terminal window
# 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:

Terminal window
# 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:

Terminal window
cp /var/lib/hitkeep/data/hitkeep.db /backups/

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

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 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.

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:

Terminal window
./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:

Terminal window
./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.