Backup & Restore

Learn about backup strategies and restore operations for Firebase migrations.

Table of contents

  1. Overview
  2. Creating Backups
    1. Using the API
    2. Using the CLI
  3. Backup Format
  4. Automatic Backups
  5. Restoring from Backup
    1. Using the API
    2. Using the CLI
  6. Rollback with Backups
  7. Backup Strategies
    1. Strategy 1: Before Every Migration Run
    2. Strategy 2: Manual Backups
    3. Strategy 3: Scheduled Backups
    4. Strategy 4: Hybrid Approach
  8. Backup Management
    1. List Backups
    2. Delete Old Backups
  9. Storage Considerations
    1. Local Storage
    2. Cloud Storage
  10. Backup Size Optimization
    1. Exclude Unnecessary Data
    2. Compress Backups
  11. Testing Backups
  12. Disaster Recovery
    1. Complete Backup/Restore Flow

Overview

MSR Firebase provides built-in backup and restore functionality to protect your data during migrations.

Creating Backups

Using the API

import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';

const appConfig = new FirebaseConfig();
appConfig.folder = './migrations';
appConfig.tableName = 'schema_version';
appConfig.databaseUrl = process.env.FIREBASE_DATABASE_URL;
appConfig.applicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;

const runner = await FirebaseRunner.getInstance({ config: appConfig });

// Create backup
const result = await runner.backup();
console.log('Backup created:', result.backupId);
console.log('Location:', result.path);

Using the CLI

msr-firebase backup

Output:

Creating backup...
✓ Backup created: backup-1234567890.json
Location: ./backups/backup-1234567890.json
Size: 2.5 MB

Backup Format

Backups are stored as JSON files:

{
  "metadata": {
    "timestamp": 1234567890,
    "databaseURL": "https://your-project.firebaseio.com",
    "createdAt": "2024-01-15T10:30:00.000Z",
    "version": "0.1.0"
  },
  "data": {
    "users": {
      "user1": {
        "name": "John Doe",
        "email": "john@example.com"
      }
    }
  }
}

Automatic Backups

Configure automatic backups before migrations:

import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';

const appConfig = new FirebaseConfig();
appConfig.folder = './migrations';
appConfig.tableName = 'schema_version';
appConfig.databaseUrl = process.env.FIREBASE_DATABASE_URL;
appConfig.applicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;

const runner = await FirebaseRunner.getInstance({ config: appConfig });

// Backup will be created automatically
await runner.migrate();

Restoring from Backup

Using the API

import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';

const appConfig = new FirebaseConfig();
appConfig.folder = './migrations';
appConfig.tableName = 'schema_version';
appConfig.databaseUrl = process.env.FIREBASE_DATABASE_URL;
appConfig.applicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;

const runner = await FirebaseRunner.getInstance({ config: appConfig });

// Restore from backup
await runner.restore('backup-1234567890.json');

Restore will overwrite all existing data. Use with extreme caution!

Using the CLI

msr-firebase restore backup-1234567890.json

Confirmation prompt:

⚠️  Warning: This will overwrite all data in the database!
Database: https://your-project.firebaseio.com
Backup: backup-1234567890.json (created 2024-01-15 10:30:00)

Are you sure you want to continue? (y/N): y

Restoring backup...
✓ Backup restored successfully

Rollback with Backups

MSR automatically creates and restores backups during migrations:

import { FirebaseRunner, FirebaseConfig } from '@migration-script-runner/firebase';

const appConfig = new FirebaseConfig();
appConfig.folder = './migrations';
appConfig.tableName = 'schema_version';
appConfig.databaseUrl = process.env.FIREBASE_DATABASE_URL;
appConfig.applicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;

const runner = await FirebaseRunner.getInstance({ config: appConfig });

// Backup is created automatically before migrating
await runner.migrate();

// If migration fails, backup is restored automatically

// Manual rollback also uses backup
await runner.down();

Backup Strategies

Strategy 1: Before Every Migration Run

config: {
  rollbackStrategy: 'backup',
  backupPath: './backups'
}

Pros: Maximum safety Cons: Slower, requires storage

Strategy 2: Manual Backups

// Create backup manually before important migrations
await runner.backup();

// Run migrations
await runner.migrate();

Pros: Full control Cons: Requires discipline

Strategy 3: Scheduled Backups

import { schedule } from 'node-cron';

// Backup every day at 2 AM
schedule('0 2 * * *', async () => {
  await runner.backup();
});

Pros: Regular backups independent of migrations Cons: May not align with migration timing

Strategy 4: Hybrid Approach

config: {
  rollbackStrategy: 'both' // Use down() if available, backup as fallback
}

Pros: Fast rollback with safety net Cons: More complex

Backup Management

List Backups

import { readdirSync } from 'fs';
import { join } from 'path';

const backupDir = './backups';
const backups = readdirSync(backupDir)
  .filter(f => f.startsWith('backup-') && f.endsWith('.json'))
  .map(f => {
    const stat = statSync(join(backupDir, f));
    return {
      file: f,
      size: stat.size,
      created: stat.birthtime
    };
  })
  .sort((a, b) => b.created.getTime() - a.created.getTime());

console.log('Available backups:', backups);

Delete Old Backups

import { unlinkSync, statSync } from 'fs';
import { join } from 'path';

function deleteOldBackups(backupDir: string, keepLast: number = 10) {
  const backups = readdirSync(backupDir)
    .filter(f => f.startsWith('backup-') && f.endsWith('.json'))
    .map(f => ({
      file: f,
      path: join(backupDir, f),
      created: statSync(join(backupDir, f)).birthtime
    }))
    .sort((a, b) => b.created.getTime() - a.created.getTime());

  // Keep only last N backups
  backups.slice(keepLast).forEach(backup => {
    unlinkSync(backup.path);
    console.log('Deleted old backup:', backup.file);
  });
}

deleteOldBackups('./backups', 10);

Storage Considerations

Local Storage

config: {
  backupPath: './backups' // Local directory
}

Pros: Fast, simple Cons: Limited space, not persistent across deployments

Cloud Storage

Upload backups to cloud storage:

import { Storage } from '@google-cloud/storage';

async function uploadBackup(localPath: string, backupId: string) {
  const storage = new Storage();
  const bucket = storage.bucket('my-backups');

  await bucket.upload(localPath, {
    destination: `firebase-backups/${backupId}`,
    metadata: {
      contentType: 'application/json'
    }
  });

  console.log('Backup uploaded to cloud storage');
}

// After creating backup
const result = await runner.backup();
await uploadBackup(result.path, result.backupId);

Backup Size Optimization

Exclude Unnecessary Data

export const up: IMigrationScript<admin.database.Database>['up'] = async (db) => {
  // Temporarily remove large data before backup
  const largeData = await db.ref('analytics').once('value');
  await db.ref('analytics').remove();

  // Migration logic...

  // Restore large data
  await db.ref('analytics').set(largeData.val());
};

Compress Backups

import { createGzip } from 'zlib';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

async function compressBackup(backupPath: string) {
  const gzip = createGzip();
  const source = createReadStream(backupPath);
  const destination = createWriteStream(`${backupPath}.gz`);

  await pipeline(source, gzip, destination);
  console.log('Backup compressed');
}

Testing Backups

Always verify backups are valid:

import { readFileSync } from 'fs';

function validateBackup(backupPath: string): boolean {
  try {
    const backup = JSON.parse(readFileSync(backupPath, 'utf-8'));

    // Verify structure
    if (!backup.metadata || !backup.data) {
      throw new Error('Invalid backup structure');
    }

    // Verify metadata
    if (!backup.metadata.timestamp || !backup.metadata.databaseURL) {
      throw new Error('Invalid backup metadata');
    }

    return true;
  } catch (error) {
    console.error('Backup validation failed:', error);
    return false;
  }
}

Disaster Recovery

Complete Backup/Restore Flow

// 1. Create backup before risky operation
const backup = await runner.backup();
console.log('Backup created:', backup.backupId);

try {
  // 2. Perform risky migration
  await runner.migrate();

  // 3. Validate results
  const snapshot = await db.ref('users').once('value');
  if (!snapshot.exists()) {
    throw new Error('Migration produced invalid state');
  }

  console.log('Migration successful');
} catch (error) {
  console.error('Migration failed, restoring backup...');

  // 4. Restore on failure
  await runner.restore(backup.backupId);

  console.log('Database restored from backup');
  throw error;
}