Skip to content

EquiTrail — Comprehensive Backup Plan

Owner: Nossie Consultancy B.V. · Last updated: 2026-06-01 All backups must be verified quarterly. See disaster_recovery.md for restore procedures.


1. What We're Protecting

Tier 1 — Irreplaceable (loss = permanent damage)

Asset Why irreplaceable
android/equitrail.jks Lose this = can never update Play Store app again
credentials.env All passwords, API keys, tokens
android/key.properties Keystore passwords

Tier 2 — Painful to lose (hours to restore from scratch)

Asset Recovery effort without backup
Firebase user data (Firestore) Users lose all rides, horses, history
Firebase Storage (ride photos) Users lose all uploaded photos
android/play_service_account.json Can regenerate in GCP console (~30 min)
android/app/google-services.json Re-downloadable from Firebase console
ios/Runner/GoogleService-Info.plist Re-downloadable from Firebase console
Discord route metadata + GPX files Community-contributed, cannot recreate
Oracle server bot data (discord_links.json) Discord↔EquiTrail ID mappings

Tier 3 — In Git (low risk, recoverable from Azure DevOps)

Asset Notes
All source code In Azure DevOps; also local clone
Website HTML files In git website/
Discord bot code In git discord_bot/
Documentation In git docs/
Build scripts In git scripts/

2. Backup Strategy Overview

┌──────────────────────────────────────────────────────────────────┐
│ Source: Mac workdir /Users/nossie/app/equitrail                  │
│                                                                  │
│  Tier 1 secrets ─────────────────────► Oracle backup server      │
│  Tier 2 config files ────────────────► Oracle backup server      │
│  Docs + chats ───────────────────────► Oracle backup server      │
│                                         (100.126.14.49)          │
│                                         incremental, 30 snaps    │
│                                                                  │
│  Source code ────────────────────────► Azure DevOps (primary)    │
│                                    └──► [GitHub mirror — TODO]   │
│                                                                  │
│  Firebase Firestore ─────────────────► GCS bucket (daily export) │
│  Firebase Storage ───────────────────► GCS bucket (policy copy)  │
│                                                                  │
│  android/equitrail.jks ──────────────► Oracle + 1Password/USB   │
│                          (offline 3rd copy — MANUAL)             │
└──────────────────────────────────────────────────────────────────┘

3. Backup Schedule

What Frequency Trigger Retention Location
Secrets + docs (rsync) Per session Claude Code stop hook 30 snapshots Oracle 100.126.14.49
Firestore export Daily 03:00 CET Cloud Scheduler 30 days GCS gs://equitrail-backups/
Firebase Storage Daily 03:00 CET GCS Transfer 30 days GCS gs://equitrail-media-backup/
Git repository Continuous Every git push Permanent Azure DevOps
Git repository Each session git push github main Permanent GitHub (mirror — TODO)
Keystore offline Quarterly Manual Permanent 1Password + USB
Discord routes + GPX Per session rsync in backup hook 30 snapshots Oracle 100.126.14.49

4. Current Implementation: scripts/backup_to_server.sh

What it backs up

credentials.env
android/equitrail.jks
android/key.properties
android/play_service_account.json
android/app/google-services.json
ios/Runner/GoogleService-Info.plist
docs/                        (including chat exports)
CLAUDE.md
CHANGELOG.md
ROADMAP.md
discord_bot/discord_links.json

How it runs

  • Auto-triggered by Claude Code stop hook on every session exit
  • Incremental rsync with --link-dest (hardlinks = unchanged files cost 0 extra disk)
  • Keeps 30 snapshots; oldest auto-pruned
  • Target: equitrail@100.126.14.49:~/equitrail-backup/YYYYMMDD_HHMMSS/
  • Symlink latest always points to most recent snapshot

Verification

# Check backups exist
ssh equitrail@100.126.14.49 "ls -lt ~/equitrail-backup/ | head -10"

# Verify keystore in latest backup
ssh equitrail@100.126.14.49 "ls -la ~/equitrail-backup/latest/android/equitrail.jks"

# Count snapshots
ssh equitrail@100.126.14.49 "ls ~/equitrail-backup/ | wc -l"

# Check backup size
ssh equitrail@100.126.14.49 "du -sh ~/equitrail-backup/"

5. Firestore Backup (TODO — implement this)

Setup (one-time, in GCP console)

Step 1: Create GCS bucket

gcloud storage buckets create gs://equitrail-backups \
  --location=europe-west4 \
  --project=equitrail

Step 2: Grant Firestore export permissions

gcloud projects add-iam-policy-binding equitrail \
  --member="serviceAccount:service-967800454287@gcp-sa-firestore.iam.gserviceaccount.com" \
  --role="roles/datastore.importExportAdmin"

gcloud storage buckets add-iam-policy-binding gs://equitrail-backups \
  --member="serviceAccount:service-967800454287@gcp-sa-firestore.iam.gserviceaccount.com" \
  --role="roles/storage.admin"

Step 3: Create Cloud Scheduler job - In GCP Console → Cloud Scheduler → Create Job - Schedule: 0 3 * * * (daily 03:00 UTC = 04:00/05:00 CET) - Target: HTTP POST - URL: https://firestore.googleapis.com/v1/projects/equitrail/databases/(default):exportDocuments - Body: {"outputUriPrefix": "gs://equitrail-backups/"} - Auth: OIDC with Firestore service account

Manual export (on-demand):

gcloud firestore export gs://equitrail-backups/manual_$(date +%Y%m%d) --project=equitrail

Restore from export:

gcloud firestore import gs://equitrail-backups/YYYYMMDD/ --project=equitrail


6. Firebase Storage Backup (Client Media)

User ride photos are stored in Firebase Storage at gs://equitrail.appspot.com/users/{uid}/.

Setup GCS Transfer Job (one-time)

# Create destination bucket
gcloud storage buckets create gs://equitrail-media-backup \
  --location=europe-west4 \
  --project=equitrail

# Set retention policy (30 days object lifecycle)
gcloud storage buckets update gs://equitrail-media-backup \
  --lifecycle-file=docs/gcs_lifecycle.json

docs/gcs_lifecycle.json:

{
  "rule": [
    {
      "action": {"type": "Delete"},
      "condition": {"age": 30, "isLive": false}
    }
  ]
}

Transfer via Storage Transfer Service (GCP Console)

  • Source: gs://equitrail.appspot.com
  • Destination: gs://equitrail-media-backup
  • Schedule: Daily at 03:30 CET
  • Options: Overwrite never, delete source never

7. Discord Routes Backup

GPX files and metadata stored on Oracle server at /home/equitrail/equitrail-bot/routes/.

These are included in the session backup via scripts/backup_discord_routes.sh:

#!/bin/bash
# Backup Discord bot routes from Oracle server to local workdir
# Then the main backup_to_server.sh picks them up on next backup

REMOTE="equitrail@100.126.14.49"
LOCAL="/Users/nossie/app/equitrail/discord_bot/routes_backup/"

mkdir -p "$LOCAL"
rsync -az "$REMOTE:~/equitrail-bot/routes/" "$LOCAL"
echo "✅ Discord routes synced: $LOCAL"

8. EXIF Metadata on Client Photos (#98 — TODO in app)

When users take photos during a ride, the app should embed: - ImageDescription: EquiTrail ride photo - Software: EquiTrail v{version} - GPSLatitude / GPSLongitude: Current ride location - DateTimeOriginal: Timestamp of capture - Copyright: © {year} {rider_name} via EquiTrail

Implementation: Use image package in Flutter before uploading to Firebase Storage. File: lib/features/tracking/data/services/photo_service.dart (to create).


9. Social Media Watermark (#99 — TODO in app)

When a user shares a ride/photo to social media: 1. Overlay a branded EquiTrail card at the bottom of the image 2. Card contains: horse name, distance, duration, date, equitrail.horse URL 3. Apply transparent EquiTrail logo watermark (bottom-right corner) 4. Auto-append hashtags: #EquiTrail #paardrijden #rijden

Implementation: image package for compositing, triggered from share button. File: lib/core/services/media_share_service.dart (to create).


10. Offline Keystore Copy (Manual — Action Required)

The Android keystore must have a third offline copy beyond the workdir and Oracle server.

Action items (do this now): 1. Open 1Password → create secure note "EquiTrail Android Keystore" 2. Attach android/equitrail.jks as file attachment 3. Add note: storepass=VYdUKs9MbUH5mFeegSMB keyAlias=equitrail 4. Optionally: copy to encrypted USB drive stored securely

Verification command:

keytool -list -v -keystore android/equitrail.jks -storepass VYdUKs9MbUH5mFeegSMB 2>/dev/null | grep -E "Alias|Valid|SHA"


11. Backup Verification Script

scripts/verify_backup.sh — run this monthly:

#!/bin/bash
# EquiTrail — Backup verification
REMOTE="equitrail@100.126.14.49"
echo "=== EquiTrail Backup Health Check ==="

# 1. Oracle backup — latest timestamp
LATEST=$(ssh -o BatchMode=yes "$REMOTE" "readlink -f ~/equitrail-backup/latest" 2>/dev/null)
echo "Latest backup: $LATEST"

# 2. Keystore in backup
KS=$(ssh -o BatchMode=yes "$REMOTE" "ls -la ~/equitrail-backup/latest/android/equitrail.jks 2>/dev/null | awk '{print \$5,\$9}'")
echo "Keystore: ${KS:-❌ MISSING}"

# 3. credentials.env in backup
CREDS=$(ssh -o BatchMode=yes "$REMOTE" "ls -la ~/equitrail-backup/latest/credentials.env 2>/dev/null | awk '{print \$5}'")
echo "credentials.env: ${CREDS:-❌ MISSING} bytes"

# 4. Snapshot count
COUNT=$(ssh -o BatchMode=yes "$REMOTE" "ls ~/equitrail-backup/ | grep -v latest | wc -l")
echo "Snapshots: $COUNT"

# 5. Total backup size
SIZE=$(ssh -o BatchMode=yes "$REMOTE" "du -sh ~/equitrail-backup/" 2>/dev/null | cut -f1)
echo "Total size: $SIZE"

echo "=== Done ==="

Related: docs/disaster_recovery.md · scripts/backup_to_server.sh · scripts/verify_backup.sh