From 04ab995e97fbc09ff75817d89faada04f86794f9 Mon Sep 17 00:00:00 2001 From: Akshay Date: Mon, 24 Nov 2025 22:30:01 +0530 Subject: [PATCH] feat: add complete Supabase disaster recovery system (backup + restore + GitHub Actions) --- .github/workflows/backup.yml | 103 +++++++++++ .gitignore | 2 + backup.sh | 350 +++++++++++++++++++++++++++++++++++ restore.sh | 336 +++++++++++++++++++++++++++++++++ supabase_snapshot/README.md | 247 ++++++++++++++++++++++++ 5 files changed, 1038 insertions(+) create mode 100644 .github/workflows/backup.yml create mode 100755 backup.sh create mode 100755 restore.sh create mode 100644 supabase_snapshot/README.md diff --git a/.github/workflows/backup.yml b/.github/workflows/backup.yml new file mode 100644 index 00000000..d54a4507 --- /dev/null +++ b/.github/workflows/backup.yml @@ -0,0 +1,103 @@ +name: Supabase Disaster Recovery Backup + +on: + # Run daily at 02:00 IST (20:30 UTC previous day) + schedule: + - cron: '30 20 * * *' + + # Allow manual trigger + workflow_dispatch: + +jobs: + backup: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install PostgreSQL client tools + run: | + sudo apt-get update + sudo apt-get install -y postgresql-client + + - name: Verify tools installation + run: | + psql --version + pg_dump --version + pg_restore --version + + - name: Create backup directory + run: mkdir -p supabase_snapshot + + - name: Run backup script + env: + SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }} + run: | + chmod +x backup.sh + ./backup.sh + + - name: List backup files + run: | + echo "Backup files created:" + ls -lh supabase_snapshot/ + + - name: Upload backup artifacts + uses: actions/upload-artifact@v4 + with: + name: supabase-backup-${{ github.run_number }}-${{ github.run_attempt }} + path: supabase_snapshot/ + retention-days: 30 + compression-level: 9 + + - name: Commit SQL files to repository (optional) + if: success() + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + # Add only SQL files (exclude large binary dump) + git add supabase_snapshot/*.sql || true + git add supabase_snapshot/backup_info.txt || true + + # Check if there are changes to commit + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "chore: update Supabase backup SQL files [skip ci]" + git push + fi + + - name: Generate job summary + if: always() + run: | + echo "# Supabase Backup Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Date:** $(date)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f supabase_snapshot/backup_info.txt ]; then + echo "## Backup Information" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat supabase_snapshot/backup_info.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Backup Files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File | Size |" >> $GITHUB_STEP_SUMMARY + echo "|------|------|" >> $GITHUB_STEP_SUMMARY + + for file in supabase_snapshot/*; do + if [ -f "$file" ]; then + filename=$(basename "$file") + size=$(du -h "$file" | cut -f1) + echo "| $filename | $size |" >> $GITHUB_STEP_SUMMARY + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Backup completed successfully!" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 016f22bc..95976ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,6 @@ next-env.d.ts !README.md !readme.md *.sql +!supabase_snapshot/*.sql +*.dump .env.local diff --git a/backup.sh b/backup.sh new file mode 100755 index 00000000..e9207c25 --- /dev/null +++ b/backup.sh @@ -0,0 +1,350 @@ +#!/bin/bash + +################################################################################ +# Supabase Disaster Recovery - Backup Script +################################################################################ +# This script creates a complete backup of your Supabase database including: +# - Schema (tables, indexes, constraints, relations) +# - RLS Policies +# - SQL Functions +# - Triggers +# - Extensions +# - Full binary data dump +# - Optional seed data +# +# Usage: +# export SUPABASE_DB_URL="postgresql://postgres:[password]@[host]:[port]/postgres" +# ./backup.sh +################################################################################ + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +BACKUP_DIR="supabase_snapshot" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + +# Optional: Define tables to include in seed data export +# Example: SEED_TABLES=("users" "roles" "settings") +SEED_TABLES=() + +################################################################################ +# Helper Functions +################################################################################ + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +check_requirements() { + log_info "Checking requirements..." + + # Check if SUPABASE_DB_URL is set + if [ -z "$SUPABASE_DB_URL" ]; then + log_error "SUPABASE_DB_URL environment variable is not set" + log_error "Please set it with: export SUPABASE_DB_URL='postgresql://postgres:[password]@[host]:[port]/postgres'" + exit 1 + fi + + # Check if required tools are installed + local missing_tools=() + + if ! command -v psql &> /dev/null; then + missing_tools+=("psql") + fi + + if ! command -v pg_dump &> /dev/null; then + missing_tools+=("pg_dump") + fi + + if [ ${#missing_tools[@]} -ne 0 ]; then + log_error "Missing required tools: ${missing_tools[*]}" + log_error "Please install PostgreSQL client tools" + exit 1 + fi + + log_info "✓ All requirements met" +} + +create_backup_directory() { + log_info "Creating backup directory..." + mkdir -p "$BACKUP_DIR" + log_info "✓ Backup directory ready: $BACKUP_DIR" +} + +################################################################################ +# Backup Functions +################################################################################ + +backup_extensions() { + log_info "Exporting extensions..." + + psql "$SUPABASE_DB_URL" -t -c " + SELECT 'CREATE EXTENSION IF NOT EXISTS \"' || extname || '\";' + FROM pg_extension + WHERE extname NOT IN ('plpgsql') + ORDER BY extname; + " > "$BACKUP_DIR/extensions.sql" + + if [ $? -eq 0 ]; then + log_info "✓ Extensions exported to $BACKUP_DIR/extensions.sql" + else + log_error "Failed to export extensions" + exit 1 + fi +} + +backup_schema() { + log_info "Exporting schema (tables, indexes, constraints, relations)..." + + pg_dump "$SUPABASE_DB_URL" \ + --schema-only \ + --no-owner \ + --no-privileges \ + --exclude-schema=storage \ + --exclude-schema=auth \ + --exclude-schema=realtime \ + --exclude-schema=supabase_functions \ + --file="$BACKUP_DIR/schema.sql" + + if [ $? -eq 0 ]; then + log_info "✓ Schema exported to $BACKUP_DIR/schema.sql" + else + log_error "Failed to export schema" + exit 1 + fi +} + +backup_policies() { + log_info "Exporting RLS policies..." + + psql "$SUPABASE_DB_URL" -t -c " + SELECT + 'CREATE POLICY ' || quote_ident(pol.polname) || + ' ON ' || quote_ident(n.nspname) || '.' || quote_ident(c.relname) || + CASE + WHEN pol.polpermissive THEN ' AS PERMISSIVE' + ELSE ' AS RESTRICTIVE' + END || + ' FOR ' || + CASE pol.polcmd + WHEN 'r' THEN 'SELECT' + WHEN 'a' THEN 'INSERT' + WHEN 'w' THEN 'UPDATE' + WHEN 'd' THEN 'DELETE' + WHEN '*' THEN 'ALL' + END || + ' TO ' || + CASE + WHEN pol.polroles = '{0}' THEN 'PUBLIC' + ELSE array_to_string(ARRAY( + SELECT rolname FROM pg_roles WHERE oid = ANY(pol.polroles) + ), ', ') + END || + CASE WHEN pol.polqual IS NOT NULL + THEN ' USING (' || pg_get_expr(pol.polqual, pol.polrelid) || ')' + ELSE '' + END || + CASE WHEN pol.polwithcheck IS NOT NULL + THEN ' WITH CHECK (' || pg_get_expr(pol.polwithcheck, pol.polrelid) || ')' + ELSE '' + END || ';' + FROM pg_policy pol + JOIN pg_class c ON pol.polrelid = c.oid + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'auth', 'storage', 'realtime', 'supabase_functions') + ORDER BY n.nspname, c.relname, pol.polname; + " > "$BACKUP_DIR/policies.sql" + + if [ $? -eq 0 ]; then + log_info "✓ RLS policies exported to $BACKUP_DIR/policies.sql" + else + log_error "Failed to export RLS policies" + exit 1 + fi +} + +backup_functions() { + log_info "Exporting SQL functions..." + + pg_dump "$SUPABASE_DB_URL" \ + --schema-only \ + --no-owner \ + --no-privileges \ + --exclude-schema=storage \ + --exclude-schema=auth \ + --exclude-schema=realtime \ + --exclude-schema=supabase_functions \ + --section=pre-data \ + --section=post-data \ + | grep -A 1000 "CREATE FUNCTION\|CREATE OR REPLACE FUNCTION" \ + > "$BACKUP_DIR/functions.sql" + + # If no functions found, create empty file + if [ ! -s "$BACKUP_DIR/functions.sql" ]; then + echo "-- No custom functions found" > "$BACKUP_DIR/functions.sql" + fi + + log_info "✓ Functions exported to $BACKUP_DIR/functions.sql" +} + +backup_triggers() { + log_info "Exporting triggers..." + + psql "$SUPABASE_DB_URL" -t -c " + SELECT pg_get_triggerdef(oid) || ';' + FROM pg_trigger + WHERE tgisinternal = false + AND tgrelid IN ( + SELECT oid FROM pg_class + WHERE relnamespace IN ( + SELECT oid FROM pg_namespace + WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'auth', 'storage', 'realtime', 'supabase_functions') + ) + ) + ORDER BY tgname; + " > "$BACKUP_DIR/triggers.sql" + + # If no triggers found, create empty file + if [ ! -s "$BACKUP_DIR/triggers.sql" ]; then + echo "-- No custom triggers found" > "$BACKUP_DIR/triggers.sql" + fi + + log_info "✓ Triggers exported to $BACKUP_DIR/triggers.sql" +} + +backup_full_data() { + log_info "Creating full binary backup (this may take a while)..." + + pg_dump "$SUPABASE_DB_URL" \ + --format=custom \ + --no-owner \ + --no-privileges \ + --exclude-schema=storage \ + --exclude-schema=auth \ + --exclude-schema=realtime \ + --exclude-schema=supabase_functions \ + --file="$BACKUP_DIR/complete_backup.dump" + + if [ $? -eq 0 ]; then + local size=$(du -h "$BACKUP_DIR/complete_backup.dump" | cut -f1) + log_info "✓ Full backup created: $BACKUP_DIR/complete_backup.dump ($size)" + else + log_error "Failed to create full backup" + exit 1 + fi +} + +backup_seed_data() { + if [ ${#SEED_TABLES[@]} -eq 0 ]; then + log_warn "No seed tables configured, skipping seed data export" + echo "-- No seed tables configured" > "$BACKUP_DIR/seed_data.sql" + return + fi + + log_info "Exporting seed data for tables: ${SEED_TABLES[*]}..." + + local table_args="" + for table in "${SEED_TABLES[@]}"; do + table_args="$table_args --table=$table" + done + + pg_dump "$SUPABASE_DB_URL" \ + --data-only \ + --no-owner \ + --no-privileges \ + --column-inserts \ + $table_args \ + --file="$BACKUP_DIR/seed_data.sql" + + if [ $? -eq 0 ]; then + log_info "✓ Seed data exported to $BACKUP_DIR/seed_data.sql" + else + log_error "Failed to export seed data" + exit 1 + fi +} + +create_backup_metadata() { + log_info "Creating backup metadata..." + + cat > "$BACKUP_DIR/backup_info.txt" << EOF +Supabase Backup Information +=========================== +Backup Date: $(date) +Timestamp: $TIMESTAMP + +Database Connection: ${SUPABASE_DB_URL%%@*}@*** + +Files Generated: +- extensions.sql : Database extensions +- schema.sql : Complete schema (tables, indexes, constraints) +- policies.sql : Row Level Security policies +- functions.sql : Custom SQL functions +- triggers.sql : Database triggers +- complete_backup.dump : Full binary backup with all data +- seed_data.sql : Seed data (if configured) + +Restore Instructions: +1. Create a new Supabase project +2. Get the connection string from project settings +3. Run: export SUPABASE_DB_URL="your-new-connection-string" +4. Run: ./restore.sh + +For more information, see README.md +EOF + + log_info "✓ Backup metadata created" +} + +################################################################################ +# Main Execution +################################################################################ + +main() { + echo "" + log_info "==========================================" + log_info "Supabase Disaster Recovery - Backup" + log_info "==========================================" + echo "" + + check_requirements + create_backup_directory + + echo "" + log_info "Starting backup process..." + echo "" + + backup_extensions + backup_schema + backup_policies + backup_functions + backup_triggers + backup_full_data + backup_seed_data + create_backup_metadata + + echo "" + log_info "==========================================" + log_info "✓ Backup completed successfully!" + log_info "==========================================" + log_info "Backup location: $BACKUP_DIR" + log_info "To restore, run: ./restore.sh" + echo "" +} + +# Run main function +main diff --git a/restore.sh b/restore.sh new file mode 100755 index 00000000..2795f9e4 --- /dev/null +++ b/restore.sh @@ -0,0 +1,336 @@ +#!/bin/bash + +################################################################################ +# Supabase Disaster Recovery - Restore Script +################################################################################ +# This script restores a complete Supabase database backup including: +# - Extensions +# - Schema (tables, indexes, constraints, relations) +# - Full data +# - SQL Functions +# - Triggers +# - RLS Policies +# +# Usage: +# export SUPABASE_DB_URL="postgresql://postgres:[password]@[host]:[port]/postgres" +# ./restore.sh +# +# IMPORTANT: This should be run on a NEW/EMPTY Supabase project +################################################################################ + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +BACKUP_DIR="supabase_snapshot" + +################################################################################ +# Helper Functions +################################################################################ + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_step() { + echo -e "${BLUE}[STEP]${NC} $1" +} + +check_requirements() { + log_info "Checking requirements..." + + # Check if SUPABASE_DB_URL is set + if [ -z "$SUPABASE_DB_URL" ]; then + log_error "SUPABASE_DB_URL environment variable is not set" + log_error "Please set it with: export SUPABASE_DB_URL='postgresql://postgres:[password]@[host]:[port]/postgres'" + exit 1 + fi + + # Check if backup directory exists + if [ ! -d "$BACKUP_DIR" ]; then + log_error "Backup directory not found: $BACKUP_DIR" + log_error "Please ensure you have run backup.sh first or the backup files are in the correct location" + exit 1 + fi + + # Check if required tools are installed + local missing_tools=() + + if ! command -v psql &> /dev/null; then + missing_tools+=("psql") + fi + + if ! command -v pg_restore &> /dev/null; then + missing_tools+=("pg_restore") + fi + + if [ ${#missing_tools[@]} -ne 0 ]; then + log_error "Missing required tools: ${missing_tools[*]}" + log_error "Please install PostgreSQL client tools" + exit 1 + fi + + log_info "✓ All requirements met" +} + +check_backup_files() { + log_info "Checking backup files..." + + local missing_files=() + local required_files=( + "$BACKUP_DIR/extensions.sql" + "$BACKUP_DIR/schema.sql" + "$BACKUP_DIR/complete_backup.dump" + ) + + for file in "${required_files[@]}"; do + if [ ! -f "$file" ]; then + missing_files+=("$file") + fi + done + + if [ ${#missing_files[@]} -ne 0 ]; then + log_error "Missing required backup files:" + for file in "${missing_files[@]}"; do + log_error " - $file" + done + exit 1 + fi + + log_info "✓ All required backup files found" +} + +confirm_restore() { + echo "" + log_warn "==========================================" + log_warn "WARNING: Database Restore Operation" + log_warn "==========================================" + log_warn "This will restore the backup to the target database." + log_warn "Target: ${SUPABASE_DB_URL%%@*}@***" + echo "" + log_warn "IMPORTANT: This should only be run on a NEW/EMPTY Supabase project!" + log_warn "Restoring to an existing database may cause conflicts." + echo "" + + read -p "Are you sure you want to continue? (yes/no): " -r + echo "" + + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + log_info "Restore cancelled by user" + exit 0 + fi +} + +test_connection() { + log_info "Testing database connection..." + + if psql "$SUPABASE_DB_URL" -c "SELECT 1;" > /dev/null 2>&1; then + log_info "✓ Database connection successful" + else + log_error "Failed to connect to database" + log_error "Please check your SUPABASE_DB_URL and ensure the database is accessible" + exit 1 + fi +} + +################################################################################ +# Restore Functions +################################################################################ + +restore_extensions() { + log_step "Step 1/6: Restoring extensions..." + + if [ ! -f "$BACKUP_DIR/extensions.sql" ]; then + log_warn "Extensions file not found, skipping..." + return + fi + + psql "$SUPABASE_DB_URL" -f "$BACKUP_DIR/extensions.sql" > /dev/null 2>&1 || { + log_warn "Some extensions may have failed to install (this is often normal)" + } + + log_info "✓ Extensions restored" +} + +restore_schema() { + log_step "Step 2/6: Restoring schema (tables, indexes, constraints)..." + + if [ ! -f "$BACKUP_DIR/schema.sql" ]; then + log_error "Schema file not found: $BACKUP_DIR/schema.sql" + exit 1 + fi + + psql "$SUPABASE_DB_URL" -f "$BACKUP_DIR/schema.sql" > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + log_info "✓ Schema restored successfully" + else + log_error "Failed to restore schema" + log_error "Check the schema.sql file for syntax errors" + exit 1 + fi +} + +restore_data() { + log_step "Step 3/6: Restoring data (this may take a while)..." + + if [ ! -f "$BACKUP_DIR/complete_backup.dump" ]; then + log_error "Backup dump file not found: $BACKUP_DIR/complete_backup.dump" + exit 1 + fi + + # Use pg_restore with data-only mode to avoid conflicts with already-created schema + pg_restore "$SUPABASE_DB_URL" \ + --data-only \ + --no-owner \ + --no-privileges \ + --disable-triggers \ + "$BACKUP_DIR/complete_backup.dump" 2>&1 | grep -v "WARNING" || true + + if [ ${PIPESTATUS[0]} -eq 0 ]; then + log_info "✓ Data restored successfully" + else + log_warn "Data restore completed with some warnings (this is often normal)" + fi +} + +restore_functions() { + log_step "Step 4/6: Restoring SQL functions..." + + if [ ! -f "$BACKUP_DIR/functions.sql" ]; then + log_warn "Functions file not found, skipping..." + return + fi + + # Check if file has actual content + if grep -q "CREATE FUNCTION\|CREATE OR REPLACE FUNCTION" "$BACKUP_DIR/functions.sql"; then + psql "$SUPABASE_DB_URL" -f "$BACKUP_DIR/functions.sql" > /dev/null 2>&1 + log_info "✓ Functions restored" + else + log_info "✓ No custom functions to restore" + fi +} + +restore_triggers() { + log_step "Step 5/6: Restoring triggers..." + + if [ ! -f "$BACKUP_DIR/triggers.sql" ]; then + log_warn "Triggers file not found, skipping..." + return + fi + + # Check if file has actual content + if grep -q "CREATE TRIGGER" "$BACKUP_DIR/triggers.sql"; then + psql "$SUPABASE_DB_URL" -f "$BACKUP_DIR/triggers.sql" > /dev/null 2>&1 + log_info "✓ Triggers restored" + else + log_info "✓ No custom triggers to restore" + fi +} + +restore_policies() { + log_step "Step 6/6: Restoring RLS policies..." + + if [ ! -f "$BACKUP_DIR/policies.sql" ]; then + log_warn "Policies file not found, skipping..." + return + fi + + # Check if file has actual content + if grep -q "CREATE POLICY" "$BACKUP_DIR/policies.sql"; then + psql "$SUPABASE_DB_URL" -f "$BACKUP_DIR/policies.sql" > /dev/null 2>&1 + log_info "✓ RLS policies restored" + else + log_info "✓ No RLS policies to restore" + fi +} + +verify_restore() { + log_info "Verifying restore..." + + # Count tables + local table_count=$(psql "$SUPABASE_DB_URL" -t -c " + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE'; + " | xargs) + + log_info "✓ Restored $table_count tables in public schema" + + # Check for data + local has_data=$(psql "$SUPABASE_DB_URL" -t -c " + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables t + JOIN pg_class c ON c.relname = t.table_name + WHERE t.table_schema = 'public' + AND t.table_type = 'BASE TABLE' + AND c.reltuples > 0 + LIMIT 1 + ); + " | xargs) + + if [ "$has_data" = "t" ]; then + log_info "✓ Data verification successful" + else + log_warn "No data found in tables (this may be expected if your backup was empty)" + fi +} + +################################################################################ +# Main Execution +################################################################################ + +main() { + echo "" + log_info "==========================================" + log_info "Supabase Disaster Recovery - Restore" + log_info "==========================================" + echo "" + + check_requirements + check_backup_files + confirm_restore + test_connection + + echo "" + log_info "Starting restore process..." + echo "" + + # Restore in the correct order + restore_extensions + restore_schema + restore_data + restore_functions + restore_triggers + restore_policies + + echo "" + verify_restore + + echo "" + log_info "==========================================" + log_info "✓ Restore completed successfully!" + log_info "==========================================" + log_info "Your Supabase database has been restored from backup" + log_info "Please verify your application is working correctly" + echo "" +} + +# Run main function +main diff --git a/supabase_snapshot/README.md b/supabase_snapshot/README.md new file mode 100644 index 00000000..1355e50e --- /dev/null +++ b/supabase_snapshot/README.md @@ -0,0 +1,247 @@ +# Supabase Disaster Recovery System + +A complete backup and restore solution for Supabase databases, enabling full recovery within minutes in case of project pausing, locking, or deletion. + +## 🎯 Features + +This system provides comprehensive backup of: + +- ✅ All tables, indexes, constraints, and relations +- ✅ All RLS (Row Level Security) policies +- ✅ All SQL functions +- ✅ All triggers +- ✅ All extensions currently enabled +- ✅ Full data backup (binary dump) +- ✅ Optional seed data export + +## 📁 Directory Structure + +``` +. +├── backup.sh # Backup script +├── restore.sh # Restore script +├── .github/workflows/ +│ └── backup.yml # GitHub Actions workflow +└── supabase_snapshot/ # Backup files directory + ├── schema.sql # Database schema + ├── policies.sql # RLS policies + ├── functions.sql # SQL functions + ├── triggers.sql # Database triggers + ├── extensions.sql # Enabled extensions + ├── complete_backup.dump # Full binary backup + ├── seed_data.sql # Seed data (optional) + └── backup_info.txt # Backup metadata +``` + +## 🚀 Quick Start + +### Prerequisites + +1. **PostgreSQL Client Tools**: Install `psql`, `pg_dump`, and `pg_restore` + ```bash + # macOS + brew install postgresql + + # Ubuntu/Debian + sudo apt-get install postgresql-client + + # Windows + # Download from https://www.postgresql.org/download/windows/ + ``` + +2. **Supabase Connection String**: Get your database URL from Supabase project settings + - Go to Project Settings → Database + - Copy the connection string (URI format) + - Format: `postgresql://postgres:[password]@[host]:[port]/postgres` + +### Manual Backup + +```bash +# Set your Supabase database URL +export SUPABASE_DB_URL="postgresql://postgres:your-password@db.xxx.supabase.co:5432/postgres" + +# Run backup +./backup.sh +``` + +The backup will be saved in the `supabase_snapshot/` directory. + +### Manual Restore + +```bash +# Set your NEW Supabase database URL +export SUPABASE_DB_URL="postgresql://postgres:new-password@db.yyy.supabase.co:5432/postgres" + +# Run restore +./restore.sh +``` + +**⚠️ IMPORTANT**: Only run restore on a **NEW/EMPTY** Supabase project to avoid conflicts. + +## 🤖 Automated Backups with GitHub Actions + +### Setup + +1. **Add GitHub Secret**: + - Go to your repository → Settings → Secrets and variables → Actions + - Click "New repository secret" + - Name: `SUPABASE_DB_URL` + - Value: Your Supabase connection string + +2. **Enable Workflow**: + - The workflow is automatically enabled when you commit `.github/workflows/backup.yml` + - It runs daily at 02:00 IST (20:30 UTC) + +### Manual Trigger + +You can manually trigger a backup: +1. Go to Actions tab in your GitHub repository +2. Select "Supabase Disaster Recovery Backup" +3. Click "Run workflow" + +### Accessing Backups + +Backups are stored as GitHub Actions artifacts: +1. Go to Actions tab +2. Click on a completed workflow run +3. Download the artifact (valid for 30 days) + +SQL files are also committed to the repository for version control. + +## 📝 Configuration + +### Customizing Seed Data Export + +Edit `backup.sh` and modify the `SEED_TABLES` array: + +```bash +# Example: Export specific tables as seed data +SEED_TABLES=("users" "roles" "settings" "categories") +``` + +### Excluding Schemas + +By default, the following Supabase internal schemas are excluded: +- `storage` +- `auth` +- `realtime` +- `supabase_functions` + +To modify exclusions, edit the `--exclude-schema` flags in `backup.sh`. + +## 🔧 Troubleshooting + +### Connection Issues + +**Problem**: Cannot connect to database + +**Solution**: +- Verify your connection string is correct +- Ensure your IP is allowed in Supabase (Database Settings → Network Restrictions) +- Check if the database is paused (unpause it in Supabase dashboard) + +### Permission Errors + +**Problem**: Permission denied errors during restore + +**Solution**: +- Ensure you're using the `postgres` user (included in Supabase connection string) +- Verify the target database is empty/new + +### Large Backup Files + +**Problem**: Binary dump is very large + +**Solution**: +- The binary dump uses custom format with compression +- For very large databases, consider: + - Increasing GitHub Actions artifact retention + - Using external storage (S3, etc.) for binary dumps + - Running backups less frequently + +### Restore Warnings + +**Problem**: Warnings during restore about existing objects + +**Solution**: +- These are usually safe to ignore if restoring to a new project +- The scripts are designed to handle common warnings +- Check the final verification step to ensure data was restored + +## 🔐 Security Best Practices + +1. **Never commit database credentials** to your repository +2. **Use GitHub Secrets** for storing `SUPABASE_DB_URL` +3. **Rotate passwords** periodically and update the secret +4. **Limit artifact retention** to necessary duration (default: 30 days) +5. **Review access** to your repository (who can download artifacts) + +## 📊 What Gets Backed Up + +### Included +- Public schema tables and data +- Custom schemas (if any) +- All indexes and constraints +- Foreign key relationships +- RLS policies +- Custom functions and procedures +- Triggers +- Extensions (uuid-ossp, pgcrypto, etc.) + +### Excluded +- Supabase internal schemas (auth, storage, realtime) +- System catalogs +- Temporary tables +- Ownership and privilege information (restored as postgres user) + +## 🆘 Disaster Recovery Procedure + +If your Supabase project becomes unavailable: + +1. **Create a new Supabase project** + - Go to https://supabase.com + - Create a new project + - Wait for it to initialize + +2. **Get the new connection string** + - Project Settings → Database + - Copy the connection string + +3. **Download your latest backup** + - From GitHub Actions artifacts, or + - From your local `supabase_snapshot/` directory + +4. **Run the restore script** + ```bash + export SUPABASE_DB_URL="your-new-connection-string" + ./restore.sh + ``` + +5. **Update your application** + - Update environment variables with new Supabase URL + - Update API keys if needed + - Test your application + +**Estimated recovery time**: 5-15 minutes (depending on database size) + +## 📚 Additional Resources + +- [Supabase Documentation](https://supabase.com/docs) +- [PostgreSQL Backup Documentation](https://www.postgresql.org/docs/current/backup.html) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) + +## 🤝 Contributing + +Feel free to customize these scripts for your specific needs. Common modifications: +- Adjust backup schedule in `.github/workflows/backup.yml` +- Add custom validation steps +- Integrate with external storage services +- Add notification systems (email, Slack, etc.) + +## 📄 License + +This disaster recovery system is provided as-is for use with your Supabase projects. + +--- + +**Last Updated**: 2025-11-24