Skip to content

Backup Strategy

Independent backup of D1 databases and KV namespaces — outside Cloudflare.

Status Implemented
Version 1.0
Date March 2026
Classification INTERNAL
Scripts infra/ucca-infra/scripts/backup/

Priority 1 Threat Model Gap

This backup system closes the Priority 1 gap identified in the Threat Model: no independent backup of D1 databases or R2 objects. Cloudflare internal redundancy is not a backup strategy — if the account is compromised or locked, all data is lost without independent copies.


What Gets Backed Up

Resource Type ID Frequency
ops-db D1 database 00daba3d-2d65-4ae2-b85a-e56d25ec2b02 Daily
rtopacks-db D1 database 334ac8fb-9850-48c0-9da0-b56c55640e98 Daily
LEADS KV namespace d4434c5cf1b3436f9cf6882e22a70af7 Daily

Not yet backed up:

  • rtopacks-media (R2 bucket) — currently empty, not in use. Add when media uploads begin.

Where Backups Go

Google Drive via the existing Google Workspace for Education account (admin@ucca.online). Free storage, independent of Cloudflare.

Google Drive Structure

UCCA Backups/
  d1/
    ops-db/
      2026-03-05T03-00-00Z.sql.gz
      2026-03-06T03-00-00Z.sql.gz
    rtopacks-db/
      2026-03-05T03-00-00Z.sql.gz
      2026-03-06T03-00-00Z.sql.gz
  kv/
    leads/
      2026-03-05T03-00-00Z.json.gz
  manifests/
    2026-03-05T03-00-00Z.json
    2026-03-06T03-00-00Z.json

Files are named with ISO 8601 UTC timestamps. All files are gzip compressed.


How It Works

The backup system is a local shell script running on the Mac Mini. It uses wrangler to export data from Cloudflare and rclone to upload to Google Drive. No Cloudflare Workers are involved — credentials must not live inside Cloudflare.

Architecture

Mac Mini (launchd, daily 03:00 AEST)
  → wrangler d1 export (D1 → local SQL file)
  → wrangler kv key list/get (KV → local JSON file)
  → gzip compress
  → rclone copy to Google Drive
  → manifest JSON uploaded
  → local temp files cleaned up

One-Time Setup

Prerequisites

# Install rclone if not installed
brew install rclone

# Configure rclone with Google Drive
rclone config
# → New remote
# → Name: gdrive
# → Type: drive (Google Drive)
# → Follow OAuth2 browser flow for admin@ucca.online
# → Done

# Create the root folder in Google Drive
# (rclone will auto-create subfolders)

Install launchd Job

# Copy plist to LaunchAgents
cp infra/ucca-infra/scripts/backup/com.ucca.backup.plist ~/Library/LaunchAgents/

# Load the job (will run daily at 03:00 local time)
launchctl load ~/Library/LaunchAgents/com.ucca.backup.plist

# Verify it's loaded
launchctl list | grep com.ucca.backup

Uninstall launchd Job

launchctl unload ~/Library/LaunchAgents/com.ucca.backup.plist
rm ~/Library/LaunchAgents/com.ucca.backup.plist

Running Backups

Manual Backup

cd infra/ucca-infra/scripts/backup/

# Full backup
./cf-backup.sh

# Dry run (show what would happen, don't upload)
./cf-backup.sh --dry-run

Verify Backups

# Check all backups are recent and valid
./cf-verify.sh

# Verbose output
./cf-verify.sh --verbose

Verification checks:

  • Each database has a backup file in Google Drive
  • Latest backup is within 48 hours
  • File sizes are above minimum thresholds
  • Gzip integrity is valid
  • D1 backups contain SQL statements
  • KV backups contain valid JSON

Restore from Backup

# List available backups
./cf-restore.sh list d1 ops-db
./cf-restore.sh list kv leads

# Restore latest
./cf-restore.sh d1 ops-db latest
./cf-restore.sh kv leads latest

# Restore specific timestamp
./cf-restore.sh d1 rtopacks-db 2026-03-05T03-00-00Z

Destructive Operation

Restore overwrites existing data. You must type RESTORE to confirm. Test restores on a throwaway D1 database first.

KV Restore Behaviour

KV restore is additive — it writes all keys from the backup but does not delete keys that aren't in the backup. If you need a clean slate, delete the namespace and recreate it before restoring.


Retention

Manual for now. Daily backups at current data sizes are tiny (KBs). Clean up files older than 90 days periodically:

rclone delete gdrive:"UCCA Backups" --min-age 90d

Automated retention will be implemented when the backup migrates to S3 (lifecycle rules).


Logs

launchd output goes to:

~/Library/Logs/ucca-backup.log

Check for errors:

tail -50 ~/Library/Logs/ucca-backup.log

Migration Path to AWS S3

When AWS startup credits are approved:

  1. Create S3 bucket ucca-backups in ap-southeast-2
  2. Swap rclone copy gdrive:... for aws s3 cp s3://... in the script
  3. Add GitHub Actions workflow for automated daily runs (eliminates Mac Mini dependency)
  4. S3 lifecycle rules replace manual retention (auto-delete after 90 days)
  5. Enable S3 versioning for additional protection
  6. Update this document

Scripts Reference

All scripts live in infra/ucca-infra/scripts/backup/:

Script Purpose
cf-backup.sh Main backup — exports D1 and KV, uploads to Google Drive
cf-verify.sh Verification — checks backups exist, are recent, and are valid
cf-restore.sh Restore — downloads from Google Drive and restores to D1/KV
com.ucca.backup.plist launchd job definition for daily 03:00 AEST execution

Version History

Version Date Change Author
1.0 2026-03-06 Initial creation — backup strategy and scripts Claude Code