#!/usr/bin/env bash # # VYNDR — daily PostgreSQL backup. # # Runs from Coolify cron (3am ET). Dumps the Supabase database via the # provided connection string, gzips it, uploads to Cloudflare R2 via # rclone, and prunes local copies older than 7 days. Failures POST to # ntfy so we hear about them within the hour. # # Required environment: # SUPABASE_DB_URL — postgres connection string # NTFY_PORT — ntfy port for alerts (default: 8080) # NTFY_TOPIC — ntfy topic (default: vyndr-admin) # R2_REMOTE — rclone remote name targeting R2 (default: r2) # R2_BUCKET — R2 bucket path (default: vyndr-backups/daily) # # Prerequisites on the host: # pg_dump (PostgreSQL client tools) # gzip # rclone configured with R2 credentials (`rclone config`) # curl (for ntfy) set -euo pipefail DATE="$(date -u +%Y-%m-%dT%H%M%SZ)" BACKUP_DIR="${BACKUP_DIR:-/tmp/vyndr-backups}" BACKUP_FILE="${BACKUP_DIR}/vyndr-${DATE}.sql.gz" LOG_FILE="${LOG_FILE:-/var/log/vyndr-backup.log}" NTFY_PORT="${NTFY_PORT:-8080}" NTFY_TOPIC="${NTFY_TOPIC:-vyndr-admin}" R2_REMOTE="${R2_REMOTE:-r2}" R2_BUCKET="${R2_BUCKET:-vyndr-backups/daily}" RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}" log() { printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" | tee -a "$LOG_FILE" } alert() { # Fire-and-forget — never fail the script because the alerter is down. curl -s --max-time 5 -d "$1" "http://localhost:${NTFY_PORT}/${NTFY_TOPIC}" >/dev/null 2>&1 || true } if [[ -z "${SUPABASE_DB_URL:-}" ]]; then log "ERROR: SUPABASE_DB_URL is not set" alert "VYNDR backup failed: SUPABASE_DB_URL missing" exit 1 fi mkdir -p "$BACKUP_DIR" log "Starting backup → ${BACKUP_FILE}" if ! pg_dump --no-owner --no-privileges "$SUPABASE_DB_URL" | gzip > "$BACKUP_FILE"; then log "ERROR: pg_dump failed" alert "VYNDR backup FAILED at ${DATE} — pg_dump" exit 1 fi SIZE="$(du -h "$BACKUP_FILE" | cut -f1)" log "Dump complete: ${SIZE}" if ! rclone copy "$BACKUP_FILE" "${R2_REMOTE}:${R2_BUCKET}/"; then log "ERROR: rclone upload failed" alert "VYNDR backup upload FAILED at ${DATE}" exit 1 fi log "Upload to ${R2_REMOTE}:${R2_BUCKET} complete" # Prune old local copies. R2 retention is configured on the bucket; this # script doesn't try to manage remote retention to avoid accidental deletes. find "$BACKUP_DIR" -name 'vyndr-*.sql.gz' -mtime "+${RETENTION_DAYS}" -delete log "Backup ${DATE} complete (${SIZE})"