Why Automate Supabase Backups?

Manual database backups can be time-consuming and prone to errors. Automating this process offers several benefits:

  • Consistency: Ensures that backups are taken at regular intervals.
  • Security: Reduces the risk of human errors.
  • Convenience: Automatically emails the backup file for safekeeping.
  • Disaster Recovery: In case of data loss, you can restore the database quickly.

Implementing the Supabase Backup Command

1. Creating a Laravel Console Command

To create a new Artisan command, run the following:

php artisan make:command SupabaseBackupViaPDO

This will generate a command file in app/Console/Commands/. Open SupabaseBackupViaPDO.php and update it with the following code:

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Carbon;

class SupabaseBackupViaPDO extends Command
{
    protected $signature = 'supabase:backup-pdo';
    protected $description = 'Backup Supabase DB and email the .sql file';

    public function handle()
    {
        try {
            $startTime = now();
            $filename = 'supabase_backup_' . $startTime->format('Y-m-d_H-i-s') . '.sql';
            $path = storage_path("app/backups/{$filename}");

            if (!is_dir(storage_path('app/backups'))) {
                mkdir(storage_path('app/backups'), 0755, true);
            }

            Log::info('Starting Supabase database backup process');
            $pdo = DB::connection()->getPdo();

            $tables = $pdo->query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
                ->fetchAll(\PDO::FETCH_COLUMN);

            Log::info('Found ' . count($tables) . ' tables to backup');
            $sql = "-- Supabase Database Backup\n-- " . now() . "\n\n";

            foreach ($tables as $table) {
                $quotedTable = "\"$table\"";
                $sql .= "-- Table: {$table}\n";

                $columns = $pdo->prepare("SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = :table ORDER BY ordinal_position");
                $columns->execute(['table' => $table]);
                $cols = $columns->fetchAll(\PDO::FETCH_ASSOC);

                $sql .= "CREATE TABLE {$quotedTable} (\n";
                foreach ($cols as $col) {
                    $line = "  \"{$col['column_name']}\" {$col['data_type']}";
                    if ($col['column_default']) $line .= " DEFAULT {$col['column_default']}";
                    if ($col['is_nullable'] === 'NO') $line .= " NOT NULL";
                    $sql .= $line . ",\n";
                }
                $sql = rtrim($sql, ",\n") . "\n);\n\n";

                Log::info("Backing up data for table: {$table}");
                $rows = $pdo->query("SELECT * FROM {$quotedTable}")->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $row) {
                    $cols = array_map(fn($col) => "\"$col\"", array_keys($row));
                    $vals = array_map(fn($val) => $val === null ? 'NULL' : $pdo->quote($val), array_values($row));
                    $sql .= "INSERT INTO {$quotedTable} (" . implode(", ", $cols) . ") VALUES (" . implode(", ", $vals) . ");\n";
                }

                $sql .= "\n";
            }

            file_put_contents($path, $sql);
            Log::info("Backup completed successfully", ['filename' => $filename]);

            $recipientEmail = env('BACKUP_RECIPIENT_EMAIL', '[email protected]');
            Mail::raw("Database backup completed successfully. Please find the attached backup file.", function ($message) use ($path, $filename, $recipientEmail) {
                $message->to($recipientEmail)
                    ->subject('Database Backup Completed')
                    ->attach($path, ['as' => $filename, 'mime' => 'application/sql']);
            });

            Log::info("Backup email sent successfully to {$recipientEmail}");
            $this->info("✅ Database backup completed and email sent.");
        } catch (\Throwable $th) {
            Log::error('Backup failed', ['error' => $th->getMessage()]);
            $this->error('Backup failed: ' . $th->getMessage());
        }
    }
}

2. Scheduling the Command

To automate this process, add the command to Laravel's scheduler in app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->command('supabase:backup-pdo')->dailyAt('02:00');
}

This will run the backup every day at 2 AM.

3. Configuring Email Settings

Ensure that your .env file includes proper mail configuration:

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="Database Backup System"
[email protected]

4. Running the Backup Manually

To test the backup manually, run:

php artisan supabase:backup-pdo

If successful, you'll receive an email with the .sql backup file attached.