Demo mode. Sample data, Gmail sends are dry runs (the exact API request is recorded, nothing leaves), no login. The sample data resets once a day, or now with the button.

Backup and recovery

Everything the system owns lives in two places:

What Where Contains
Data folder DATA_DIR (default ./data) deanreach.db (SQLite: contacts, history, settings), gmail-token.json, credentials.json, uploads/ (CVs)
Configuration .env next to app.py admin login, secret key, paths, transport

The source code is in your git repository; it does not need backing up separately.

Automatic backups

scripts/backup.sh writes a timestamped archive of the data folder using SQLite's online backup command (safe while the app is running), keeps the last 30 archives, and prints the restore command:

./scripts/backup.sh                 # -> backups/deanreach-2026-09-08T0200.tar.gz

Run it nightly:

crontab -e
0 2 * * * cd /opt/deanreach && ./scripts/backup.sh >> backups/backup.log 2>&1

Copy the backups/ folder off the machine (rsync to another host, a cloud bucket, or a Google Drive sync folder). Whatever you already use for other files is fine; the archives are small (a few MB).

With Docker Compose the same script runs on the host against ./data.

Restore

  1. Stop the app (docker compose down or systemctl stop deanreach).
  2. ./scripts/restore.sh backups/deanreach-2026-09-08T0200.tar.gz (it moves the current data folder aside as data.before-restore-<time> first).
  3. Start the app. Sign in, open the dashboard: contacts, history and settings are back. The Gmail connection is part of the data folder, so it comes back too.

Moving to a new server

Install on the new machine (Install and deploy), copy .env and the latest backup, run the restore script, start. Update the OAuth redirect URI in Google Cloud if the host name changed.

PostgreSQL

If DATABASE_URL points at Postgres, back the database up with pg_dump on your usual schedule and back up the data folder (token, CVs, client file) with the script above.

What to test twice a year

Restore a backup onto a laptop with TRANSPORT=dryrun and check that the contact count and the latest history rows match production. A backup nobody has restored is a hope, not a backup.