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