# 14 - Migration Guide ## 14.1 Overview This document provides a comprehensive guide for migrating OpenWA, including: - Database migration (SQLite → PostgreSQL) - Version upgrades (v0.1 → v0.2 → v1.0) - Transfer session authentication state - Rollback procedures ```mermaid flowchart TB subgraph Migration Types DB[Database Migration] VER[Version Upgrade] ENV[Environment Migration] end subgraph Targets DB --> PG[SQLite → PostgreSQL] DB --> SCALE[Single → Multi-node] VER --> MINOR[Minor Upgrade] VER --> MAJOR[Major Upgrade] ENV --> DEV[Dev → Staging] ENV --> PROD[Staging → Production] end ``` ## 14.2 Pre-Migration Checklist ### Universal Checklist ```markdown ## Pre-Migration Checklist ### Backup - [ ] Database backup completed - [ ] Session auth files backed up (.wwebjs_auth/) - [ ] Environment variables documented - [ ] Docker volumes backed up (if applicable) ### Documentation - [ ] Current version documented - [ ] Active sessions list exported - [ ] Webhook configurations exported - [ ] API keys documented ### Communication - [ ] Maintenance window scheduled - [ ] Users notified - [ ] Rollback plan prepared - [ ] Support team briefed ### Verification - [ ] Target environment ready - [ ] Network connectivity tested - [ ] Disk space sufficient (2x current size) - [ ] New version tested in staging ``` ## 14.3 Database Migration: SQLite → PostgreSQL ### When to Migrate ```mermaid flowchart TD A[Current Setup] --> B{Check Conditions} B -->|Sessions > 5| C[Consider PostgreSQL] B -->|Messages > 100K| C B -->|Need HA| C B -->|Concurrent writes high| C B -->|Sessions ≤ 5| D[Stay with SQLite] B -->|Low volume| D C --> E[Plan Migration] D --> F[Optimize SQLite] ``` | Condition | SQLite OK | Migrate to PostgreSQL | | ------------------ | ---------- | --------------------- | | Sessions | 1-5 | 6+ | | Messages/day | < 10,000 | > 10,000 | | Concurrent users | < 10 | > 10 | | High Availability | Not needed | Required | | Horizontal scaling | Not needed | Required | ### API-Based Migration (Recommended for v0.2+) OpenWA v0.2+ includes built-in migration API endpoints that leverage the **Dual-Database Architecture**: ```bash # Step 1: Export all Data DB tables curl -s 'http://localhost:2785/api/infra/export-data' \ -H 'X-API-Key: YOUR_KEY' > data-backup.json # Step 2: Change database configuration in .env or Dashboard # From: DATABASE_TYPE=sqlite # To: DATABASE_TYPE=postgres # POSTGRES_BUILTIN=true # Step 3: Restart with new configuration docker compose --profile with-dashboard --profile with-proxy up -d # Step 4: Import data to new database curl -X POST 'http://localhost:2785/api/infra/import-data' \ -H 'X-API-Key: YOUR_KEY' \ -H 'Content-Type: application/json' \ -d @data-backup.json ``` > [!NOTE] > **Dual-Database Architecture** > > OpenWA separates databases: > > - **Main DB** (SQLite): API keys, audit logs - never migrated, always local > - **Data DB** (Pluggable): Sessions, webhooks, messages - this is what gets migrated > > See [05 - Database Design: Dual-Database Architecture](./05-database-design.md#dual-database-architecture) **Export Response Example:** ```json { "exportedAt": "2026-02-05T02:30:00.000Z", "dataDbType": "sqlite", "tables": { "sessions": [...], "webhooks": [...], "messages": [...], "messageBatches": [...] }, "counts": { "sessions": 5, "webhooks": 12, "messages": 1500, "messageBatches": 3 } } ``` ### Storage Migration (Local ↔ S3/MinIO) OpenWA v0.2+ supports migrating media files between storage backends: ```bash # Step 1: Check current storage file count curl -s 'http://localhost:2785/api/infra/storage/files/count' \ -H 'X-API-Key: YOUR_KEY' # Response: { "storageType": "local", "count": 150, "sizeBytes": 15000000 } # Step 2: Export all files as tar.gz curl -s 'http://localhost:2785/api/infra/storage/export' \ -H 'X-API-Key: YOUR_KEY' # Response: { "message": "Storage export completed", "download": "/app/data/storage-export-xxx.tar.gz" } # Step 3: Change storage configuration # From: STORAGE_TYPE=local # To: STORAGE_TYPE=s3 # MINIO_BUILTIN=true # or false for external S3 # Step 4: Restart with new configuration docker compose up -d # Step 5: Import files to new storage curl -X POST 'http://localhost:2785/api/infra/storage/import' \ -H 'X-API-Key: YOUR_KEY' \ -H 'Content-Type: application/json' \ -d '{"filePath": "/app/data/storage-export-xxx.tar.gz"}' ``` | Scenario | Support | Method | | ---------------------------- | ------- | ------------------------ | | Local → Built-in MinIO | ✅ | Export → Config → Import | | Local → External S3 | ✅ | Export → Config → Import | | Built-in MinIO → External S3 | ✅ | Export → Config → Import | | S3 → Local | ✅ | Export → Config → Import | ### Redis Migration (Cache) Redis in OpenWA is used **only for caching** with TTL-based expiration. Cache data is ephemeral and automatically regenerates from the database. **No migration API needed** - just change configuration: ```bash # Switch from built-in to external Redis REDIS_ENABLED=true REDIS_BUILTIN=false # false = external Redis REDIS_HOST=your-redis-host.com REDIS_PORT=6379 REDIS_PASSWORD=optional ``` | Scenario | Support | Notes | | ------------------------- | ------- | ---------------------------- | | Built-in → External Redis | ✅ | Config change only | | External → Built-in Redis | ✅ | Config change only | | Enable → Disable Redis | ✅ | App uses memory fallback | | Disable → Enable Redis | ✅ | Cache rebuilds automatically | > [!TIP] > **Cache Warm-up**: After switching Redis instances, the cache will automatically rebuild as requests come in. No data migration is necessary. ### BullMQ Migration (Queue System) BullMQ stores job data in Redis. When switching Redis instances, pending jobs may be lost. **Best Practice - Drain Queue Before Switching:** ```bash # Step 1: Check queue status via Bull Board # Visit: http://localhost:2785/admin/queues # Step 2: Wait until MESSAGE and WEBHOOK queues are empty # Or check via API: curl -s 'http://localhost:2785/api/infra/status' \ -H 'X-API-Key: YOUR_KEY' | jq '.queue' # Wait for: pending: 0 # Step 3: Change Redis configuration REDIS_HOST=new-redis-host.com # Step 4: Restart application docker compose up -d ``` | Scenario | Support | Notes | | ------------------------- | ------- | ----------------- | | Queue Disabled → Enabled | ✅ | Config change | | Queue Enabled → Disabled | ⚠️ | Drain queue first | | Built-in → External Redis | ⚠️ | Drain queue first | > [!WARNING] > **Job Loss Prevention**: Always ensure the MESSAGE and WEBHOOK queues are empty before switching Redis instances. Check `/admin/queues` dashboard. ### Infrastructure Migration Summary | Component | Migration Method | API Endpoint | | ------------ | -------------------- | -------------------------------------------------------- | | **Database** | Export/Import JSON | `/api/infra/export-data`, `/api/infra/import-data` | | **Storage** | Export/Import tar.gz | `/api/infra/storage/export`, `/api/infra/storage/import` | | **Redis** | Config change only | N/A (cache auto-rebuilds) | | **BullMQ** | Drain then config | N/A (wait for empty queues) | ### Migration Script (Legacy) ```typescript // scripts/migrate-sqlite-to-postgres.ts import { DataSource } from 'typeorm'; import * as sqlite3 from 'sqlite3'; import { Client } from 'pg'; interface MigrationConfig { sqlitePath: string; postgresUrl: string; batchSize: number; } interface MigrationResult { table: string; rowsMigrated: number; duration: number; errors: string[]; } async function migrateSqliteToPostgres(config: MigrationConfig): Promise { const results: MigrationResult[] = []; // 1. Connect to both databases console.log('🔌 Connecting to databases...'); const sqliteDb = new sqlite3.Database(config.sqlitePath); const pgClient = new Client({ connectionString: config.postgresUrl }); await pgClient.connect(); // 2. Get list of tables const tables = await getSqliteTables(sqliteDb); console.log(`📋 Found ${tables.length} tables to migrate`); // 3. Migration order (respect foreign keys) const migrationOrder = [ 'sessions', 'api_keys', 'webhooks', 'contacts', 'messages', 'media_files', 'webhook_logs', 'audit_logs', ]; // 4. Migrate each table for (const table of migrationOrder) { if (!tables.includes(table)) continue; const startTime = Date.now(); const result = await migrateTable(sqliteDb, pgClient, table, config.batchSize); result.duration = Date.now() - startTime; results.push(result); console.log(`✅ ${table}: ${result.rowsMigrated} rows in ${result.duration}ms`); } // 5. Reset sequences await resetPostgresSequences(pgClient, migrationOrder); // 6. Cleanup sqliteDb.close(); await pgClient.end(); return results; } async function migrateTable( sqlite: sqlite3.Database, pg: Client, table: string, batchSize: number, ): Promise { const result: MigrationResult = { table, rowsMigrated: 0, duration: 0, errors: [], }; return new Promise(resolve => { let offset = 0; const processBatch = () => { sqlite.all(`SELECT * FROM ${table} LIMIT ${batchSize} OFFSET ${offset}`, async (err, rows: any[]) => { if (err) { result.errors.push(err.message); resolve(result); return; } if (rows.length === 0) { resolve(result); return; } // Insert into PostgreSQL for (const row of rows) { try { const columns = Object.keys(row); const values = Object.values(row); const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); await pg.query( `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders}) ON CONFLICT DO NOTHING`, values, ); result.rowsMigrated++; } catch (insertErr: any) { result.errors.push(`Row error: ${insertErr.message}`); } } offset += batchSize; processBatch(); }); }; processBatch(); }); } async function resetPostgresSequences(pg: Client, tables: string[]): Promise { for (const table of tables) { try { await pg.query(` SELECT setval( pg_get_serial_sequence('${table}', 'id'), COALESCE((SELECT MAX(id) FROM ${table}), 0) + 1, false ) `); } catch (err) { // Table might not have serial id } } } function getSqliteTables(db: sqlite3.Database): Promise { return new Promise((resolve, reject) => { db.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", (err, rows: any[]) => { if (err) reject(err); else resolve(rows.map(r => r.name)); }); }); } // CLI Entry point const config: MigrationConfig = { sqlitePath: process.env.SQLITE_PATH || './data/openwa.db', postgresUrl: process.env.DATABASE_URL || 'postgresql://user:pass@localhost:5432/openwa', batchSize: parseInt(process.env.BATCH_SIZE || '1000'), }; migrateSqliteToPostgres(config) .then(results => { console.log('\n📊 Migration Summary:'); console.table( results.map(r => ({ Table: r.table, Rows: r.rowsMigrated, 'Time (ms)': r.duration, Errors: r.errors.length, })), ); }) .catch(console.error); ``` ### Step-by-Step Migration ```bash # Step 1: Stop OpenWA docker compose down # Step 2: Backup current data cp -r ./data ./data-backup-$(date +%Y%m%d) docker exec openwa-db pg_dump -U postgres openwa > backup.sql # Step 3: Setup PostgreSQL (if not exists) docker compose -f docker-compose.postgres.yml up -d postgres # Step 4: Run migration script npx ts-node scripts/migrate-sqlite-to-postgres.ts # Step 5: Update environment export DATABASE_ADAPTER=postgresql export DATABASE_URL=postgresql://user:pass@localhost:5432/openwa # Step 6: Verify migration psql $DATABASE_URL -c "SELECT COUNT(*) FROM sessions;" psql $DATABASE_URL -c "SELECT COUNT(*) FROM messages;" # Step 7: Start with PostgreSQL docker compose -f docker-compose.postgres.yml up -d # Step 8: Verify functionality curl http://localhost:2785/health ``` ### Verification Queries ```sql -- Compare row counts -- Run on both SQLite and PostgreSQL -- Sessions SELECT 'sessions' as table_name, COUNT(*) as count FROM sessions UNION ALL SELECT 'messages', COUNT(*) FROM messages UNION ALL SELECT 'contacts', COUNT(*) FROM contacts UNION ALL SELECT 'webhooks', COUNT(*) FROM webhooks UNION ALL SELECT 'api_keys', COUNT(*) FROM api_keys; -- Verify foreign key integrity SELECT m.id, m.session_id FROM messages m LEFT JOIN sessions s ON m.session_id = s.id WHERE s.id IS NULL; -- Check for data integrity SELECT session_id, COUNT(*) as msg_count FROM messages GROUP BY session_id ORDER BY msg_count DESC LIMIT 10; ``` ## 14.4 Session Auth State Transfer ### Understanding Session Auth ``` Session Auth Structure: .wwebjs_auth/ ├── session-{sessionId}/ │ ├── Default/ │ │ ├── IndexedDB/ │ │ ├── Local Storage/ │ │ └── Session Storage/ │ └── ... (Chrome profile data) ``` ### Transfer Methods #### Method 1: Direct File Copy (Same Host) ```bash #!/bin/bash # transfer-session.sh SOURCE_DIR="/old-server/data/.wwebjs_auth" TARGET_DIR="/new-server/data/.wwebjs_auth" SESSION_ID=$1 if [ -z "$SESSION_ID" ]; then echo "Usage: ./transfer-session.sh " exit 1 fi # Stop both instances echo "⏹️ Stopping services..." ssh old-server "docker compose down" ssh new-server "docker compose down" # Copy session data echo "📦 Copying session data..." rsync -avz --progress \ "old-server:${SOURCE_DIR}/session-${SESSION_ID}/" \ "${TARGET_DIR}/session-${SESSION_ID}/" # Copy database record echo "📄 Exporting session record..." ssh old-server "sqlite3 /data/openwa.db \ \"SELECT * FROM sessions WHERE id='${SESSION_ID}'\" \ -csv" > session_record.csv # Import to new database echo "📥 Importing session record..." ssh new-server "sqlite3 /data/openwa.db \ \".import session_record.csv sessions\"" # Start new server echo "▶️ Starting new server..." ssh new-server "docker compose up -d" echo "✅ Session ${SESSION_ID} transferred successfully" ``` #### Method 2: Export/Import via API ```typescript // Export session from old server // GET /api/sessions/{id}/export interface SessionExport { session: { id: string; name: string; phoneNumber: string; status: string; config: object; }; authData: string; // Base64 encoded tar.gz of auth folder webhooks: WebhookConfig[]; contacts: Contact[]; } // api/sessions/export.controller.ts @Get(':id/export') @UseGuards(ApiKeyGuard) async exportSession(@Param('id') id: string): Promise { const session = await this.sessionService.findOne(id); // Create tar.gz of auth folder const authPath = path.join(this.authDir, `session-${id}`); const tarBuffer = await this.createTarGz(authPath); return { session: { id: session.id, name: session.name, phoneNumber: session.phoneNumber, status: 'exported', config: session.config, }, authData: tarBuffer.toString('base64'), webhooks: await this.webhookService.findBySession(id), contacts: await this.contactService.findBySession(id), }; } // Import session to new server // POST /api/sessions/import @Post('import') @UseGuards(ApiKeyGuard) async importSession(@Body() data: SessionExport): Promise { // 1. Create session record const session = await this.sessionService.create({ id: data.session.id, name: data.session.name, config: data.session.config, }); // 2. Extract auth data const authBuffer = Buffer.from(data.authData, 'base64'); const authPath = path.join(this.authDir, `session-${session.id}`); await this.extractTarGz(authBuffer, authPath); // 3. Import webhooks for (const webhook of data.webhooks) { await this.webhookService.create({ ...webhook, sessionId: session.id, }); } // 4. Import contacts for (const contact of data.contacts) { await this.contactService.create({ ...contact, sessionId: session.id, }); } // 5. Initialize session (will use existing auth) await this.whatsappService.initSession(session.id); return session; } ``` ### Bulk Session Transfer ```typescript // scripts/bulk-transfer-sessions.ts interface TransferConfig { sourceUrl: string; sourceApiKey: string; targetUrl: string; targetApiKey: string; sessionIds: string[] | 'all'; } async function bulkTransferSessions(config: TransferConfig): Promise { const axios = require('axios'); // Get session list let sessionIds = config.sessionIds; if (sessionIds === 'all') { const response = await axios.get(`${config.sourceUrl}/api/sessions`, { headers: { 'X-API-Key': config.sourceApiKey }, }); sessionIds = response.data.map((s: any) => s.id); } console.log(`📋 Transferring ${sessionIds.length} sessions...`); for (const sessionId of sessionIds) { try { console.log(`\n🔄 Processing session: ${sessionId}`); // 1. Stop session on source await axios.post( `${config.sourceUrl}/api/sessions/${sessionId}/stop`, {}, { headers: { 'X-API-Key': config.sourceApiKey } }, ); // 2. Export from source const exportResponse = await axios.get(`${config.sourceUrl}/api/sessions/${sessionId}/export`, { headers: { 'X-API-Key': config.sourceApiKey }, }); // 3. Import to target await axios.post(`${config.targetUrl}/api/sessions/import`, exportResponse.data, { headers: { 'X-API-Key': config.targetApiKey }, }); console.log(`✅ Session ${sessionId} transferred`); } catch (error: any) { console.error(`❌ Failed to transfer ${sessionId}: ${error.message}`); } } } ``` ## 14.5 Version Upgrade Guide ### Upgrade Matrix ```mermaid flowchart LR V01[v0.1.x] -->|Direct| V02[v0.2.x] V02 -->|Direct| V10[v1.0.x] V01 -->|"Via v0.2"| V10 V10 -->|Direct| V11[v1.1.x] V11 -->|Direct| V12[v1.2.x] ``` | From | To | Migration Type | Downtime | | ------ | ------ | ------------------------- | --------- | | v0.1.x | v0.2.x | Schema migration | 5-10 min | | v0.2.x | v1.0.x | Schema + Config migration | 10-15 min | | v0.1.x | v1.0.x | Must go via v0.2.x | 15-25 min | | v1.x | v1.y | Usually automatic | < 5 min | ### v0.1 → v0.2 Migration #### Breaking Changes in v0.2 ```yaml breaking_changes: api: - path: /api/session → /api/sessions (plural) - path: /api/send → /api/sessions/{id}/messages - auth: Basic Auth → API Key config: - DATABASE_PATH → DATABASE_URL (for PostgreSQL) - WEBHOOK_URL → Managed via API database: - New tables: api_keys, webhook_logs - Modified: sessions (added config JSON column) - Modified: messages (added metadata column) ``` #### Migration Steps ```bash #!/bin/bash # upgrade-v01-to-v02.sh set -e echo "🚀 Upgrading OpenWA v0.1.x → v0.2.x" # 1. Backup echo "📦 Creating backup..." BACKUP_DIR="./backups/v01-$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP_DIR" cp -r ./data "$BACKUP_DIR/" cp .env "$BACKUP_DIR/" # 2. Stop current version echo "⏹️ Stopping v0.1..." docker compose down # 3. Run database migrations echo "🔄 Running migrations..." docker run --rm \ -v $(pwd)/data:/app/data \ -e DATABASE_URL=sqlite:///app/data/openwa.db \ ghcr.io/rmyndharis/openwa:0.2.0 \ npm run migration:run # 4. Migrate configuration echo "⚙️ Migrating configuration..." cat > .env.new << 'EOF' # OpenWA v0.2.x Configuration # Database (unchanged if using SQLite) DATABASE_ADAPTER=sqlite DATABASE_URL=sqlite:./data/openwa.db # New in v0.2: API Key Authentication API_KEY_ENABLED=true # Generate with: openssl rand -hex 32 API_KEY=your-generated-api-key # Webhook (now managed via API, but can set default) DEFAULT_WEBHOOK_URL=${WEBHOOK_URL:-} # Other settings migrated from v0.1 NODE_ENV=${NODE_ENV:-production} PORT=${PORT:-3000} LOG_LEVEL=${LOG_LEVEL:-info} EOF # Merge with existing .env source .env envsubst < .env.new > .env.migrated mv .env.migrated .env rm .env.new # 5. Start new version echo "▶️ Starting v0.2..." docker compose pull docker compose up -d # 6. Wait for health echo "⏳ Waiting for health check..." sleep 10 curl -f http://localhost:2785/health || exit 1 # 7. Create API key for existing integrations echo "🔑 Creating API key..." docker exec openwa npm run cli -- create-api-key --name "migrated-key" echo "✅ Upgrade complete!" echo "" echo "⚠️ IMPORTANT: Update your API clients:" echo " - Change /api/session to /api/sessions" echo " - Change /api/send to /api/sessions/{id}/messages" echo " - Add X-API-Key header to all requests" ``` ### v0.2 → v1.0 Migration #### Breaking Changes in v1.0 ```yaml breaking_changes: api: - Groups API restructured - Webhook payload format v2 - Rate limiting enforced config: - ENGINE_TYPE required (default: whatsapp-web.js) - STORAGE_ADAPTER required (default: local) database: - New tables: groups, group_participants, channels - sessions.status enum expanded ``` #### Migration Steps ```bash #!/bin/bash # upgrade-v02-to-v10.sh set -e echo "🚀 Upgrading OpenWA v0.2.x → v1.0.0" # Pre-flight checks CURRENT_VERSION=$(docker inspect ghcr.io/rmyndharis/openwa --format '{{.Config.Labels.version}}' 2>/dev/null || echo "unknown") echo "Current version: $CURRENT_VERSION" # 1. Comprehensive backup echo "📦 Creating comprehensive backup..." BACKUP_DIR="./backups/v02-$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP_DIR" # Backup database if [ "$DATABASE_ADAPTER" = "postgresql" ]; then pg_dump $DATABASE_URL > "$BACKUP_DIR/database.sql" else cp ./data/openwa.db "$BACKUP_DIR/" fi # Backup auth sessions cp -r ./data/.wwebjs_auth "$BACKUP_DIR/" # Backup config cp .env "$BACKUP_DIR/" cp docker-compose.yml "$BACKUP_DIR/" # 2. Export webhook configurations (per session, new format in v1.0) echo "📤 Exporting webhooks..." for sessionId in $(curl -s -H "X-API-Key: $API_KEY" \ http://localhost:2785/api/sessions | jq -r '.data[].id'); do curl -s -H "X-API-Key: $API_KEY" \ "http://localhost:2785/api/sessions/${sessionId}/webhooks" \ > "$BACKUP_DIR/webhooks_${sessionId}.json" done # 3. Stop services echo "⏹️ Stopping v0.2..." docker compose down # 4. Update configuration echo "⚙️ Updating configuration..." cat >> .env << 'EOF' # New in v1.0 ENGINE_TYPE=whatsapp-web.js STORAGE_ADAPTER=local CACHE_ADAPTER=memory # Webhook v2 format (recommended) WEBHOOK_VERSION=2 EOF # 5. Run database migrations echo "🔄 Running migrations..." docker run --rm \ -v $(pwd)/data:/app/data \ --env-file .env \ ghcr.io/rmyndharis/openwa:1.0.0 \ npm run migration:run # 6. Migrate webhooks to new format echo "🔄 Migrating webhooks..." docker run --rm \ -v $(pwd)/data:/app/data \ -v "$BACKUP_DIR/webhooks.json:/tmp/webhooks.json" \ --env-file .env \ ghcr.io/rmyndharis/openwa:1.0.0 \ npm run cli -- migrate-webhooks /tmp/webhooks.json # 7. Start new version echo "▶️ Starting v1.0..." sed -i 's/:0.2./:1.0./g' docker-compose.yml docker compose pull docker compose up -d # 8. Health check echo "⏳ Waiting for health check..." for i in {1..30}; do if curl -sf http://localhost:2785/health > /dev/null; then echo "✅ Health check passed" break fi sleep 2 done # 9. Verify sessions echo "🔍 Verifying sessions..." curl -s -H "X-API-Key: $API_KEY" \ http://localhost:2785/api/sessions | jq '.[] | {id, status}' echo "" echo "✅ Upgrade to v1.0.0 complete!" echo "" echo "📝 Post-upgrade tasks:" echo " 1. Update webhook consumers for v2 payload format" echo " 2. Test all active sessions" echo " 3. Review new rate limits" echo " 4. Check Groups API if used" ``` ## 14.6 Rollback Procedures ### Quick Rollback (< 24 hours) ```bash #!/bin/bash # rollback.sh BACKUP_DIR=$1 TARGET_VERSION=$2 if [ -z "$BACKUP_DIR" ] || [ -z "$TARGET_VERSION" ]; then echo "Usage: ./rollback.sh " echo "Example: ./rollback.sh ./backups/v02-20260215-120000 0.2.0" exit 1 fi echo "🔄 Rolling back to v${TARGET_VERSION}..." # 1. Stop current docker compose down # 2. Restore database echo "📥 Restoring database..." if [ -f "$BACKUP_DIR/database.sql" ]; then # PostgreSQL psql $DATABASE_URL < "$BACKUP_DIR/database.sql" else # SQLite cp "$BACKUP_DIR/openwa.db" ./data/ fi # 3. Restore auth sessions echo "📥 Restoring auth sessions..." rm -rf ./data/.wwebjs_auth cp -r "$BACKUP_DIR/.wwebjs_auth" ./data/ # 4. Restore configuration echo "📥 Restoring configuration..." cp "$BACKUP_DIR/.env" . cp "$BACKUP_DIR/docker-compose.yml" . # 5. Start old version echo "▶️ Starting v${TARGET_VERSION}..." docker compose pull docker compose up -d # 6. Verify sleep 10 curl -f http://localhost:2785/health && echo "✅ Rollback successful" ``` ### Rollback Decision Tree ```mermaid flowchart TD A[Issue Detected] --> B{Severity?} B -->|Critical| C[Immediate Rollback] B -->|High| D{Fix Available?} B -->|Medium| E{Can Wait?} D -->|Yes| F[Apply Hotfix] D -->|No| C E -->|Yes| G[Schedule Fix] E -->|No| D C --> H{Backup Age?} H -->|< 24h| I[Full Rollback] H -->|> 24h| J[Partial Rollback + Data Merge] F --> K[Monitor] I --> K J --> K G --> K ``` ## 14.7 Environment Migration ### Development → Staging ```yaml # environments/staging.yml migration: source: development target: staging steps: - name: Export sessions (anonymized) command: | npm run cli -- export-sessions \ --anonymize \ --output /tmp/sessions-staging.json - name: Reset database command: | npm run migration:fresh - name: Import test sessions command: | npm run cli -- import-sessions /tmp/sessions-staging.json - name: Configure staging webhooks command: | npm run cli -- set-webhook \ --url https://staging-webhook.example.com/openwa \ --events message,status - name: Set staging limits command: | npm run cli -- set-limits \ --max-sessions 10 \ --rate-limit 100/min ``` ### Staging → Production ```yaml # environments/production.yml migration: source: staging target: production pre_checks: - name: Staging tests passed command: npm run test:e2e -- --tag staging required: true - name: Performance benchmarks command: npm run benchmark threshold: 'p99 < 500ms' - name: Security scan command: npm audit --production required: true steps: - name: Blue-green deployment type: blue-green config: health_check: /health switch_after: 60s rollback_on_error: true - name: Gradual traffic shift type: canary config: initial_percentage: 10 increment: 10 interval: 5m success_threshold: 99% ``` ## 14.8 Data Export/Import ### Full Export ```typescript // scripts/full-export.ts interface ExportOptions { outputDir: string; includeMedia: boolean; includeLogs: boolean; compress: boolean; } async function fullExport(options: ExportOptions): Promise { const fs = require('fs-extra'); const archiver = require('archiver'); const exportDir = path.join(options.outputDir, `export-${Date.now()}`); await fs.ensureDir(exportDir); // 1. Export database tables console.log('📊 Exporting database...'); const tables = ['sessions', 'messages', 'contacts', 'webhooks', 'api_keys']; for (const table of tables) { const data = await db.query(`SELECT * FROM ${table}`); await fs.writeJson(path.join(exportDir, `${table}.json`), data, { spaces: 2 }); } // 2. Export auth sessions console.log('🔐 Exporting auth sessions...'); await fs.copy('./data/.wwebjs_auth', path.join(exportDir, 'auth')); // 3. Export media (optional) if (options.includeMedia) { console.log('📁 Exporting media files...'); await fs.copy('./data/media', path.join(exportDir, 'media')); } // 4. Export logs (optional) if (options.includeLogs) { console.log('📝 Exporting logs...'); await fs.copy('./logs', path.join(exportDir, 'logs')); } // 5. Export configuration (sanitized) console.log('⚙️ Exporting configuration...'); const config = { version: process.env.npm_package_version, exportedAt: new Date().toISOString(), settings: { DATABASE_ADAPTER: process.env.DATABASE_ADAPTER, STORAGE_ADAPTER: process.env.STORAGE_ADAPTER, ENGINE_TYPE: process.env.ENGINE_TYPE, }, }; await fs.writeJson(path.join(exportDir, 'config.json'), config, { spaces: 2 }); // 6. Compress (optional) if (options.compress) { console.log('🗜️ Compressing export...'); const output = fs.createWriteStream(`${exportDir}.tar.gz`); const archive = archiver('tar', { gzip: true }); archive.pipe(output); archive.directory(exportDir, false); await archive.finalize(); await fs.remove(exportDir); console.log(`✅ Export complete: ${exportDir}.tar.gz`); } else { console.log(`✅ Export complete: ${exportDir}`); } } ``` ### Full Import ```typescript // scripts/full-import.ts interface ImportOptions { inputPath: string; mergeStrategy: 'replace' | 'merge' | 'skip-existing'; dryRun: boolean; } async function fullImport(options: ImportOptions): Promise { const fs = require('fs-extra'); const tar = require('tar'); let importDir = options.inputPath; // Extract if compressed if (options.inputPath.endsWith('.tar.gz')) { importDir = options.inputPath.replace('.tar.gz', '-extracted'); await tar.extract({ file: options.inputPath, cwd: importDir, }); } // Validate export const configPath = path.join(importDir, 'config.json'); if (!(await fs.pathExists(configPath))) { throw new Error('Invalid export: config.json not found'); } const exportConfig = await fs.readJson(configPath); console.log(`📦 Importing from v${exportConfig.version}`); console.log(`📅 Exported at: ${exportConfig.exportedAt}`); if (options.dryRun) { console.log('🔍 DRY RUN - No changes will be made'); } // Import order matters (foreign keys) const importOrder = ['sessions', 'api_keys', 'webhooks', 'contacts', 'messages']; for (const table of importOrder) { const dataPath = path.join(importDir, `${table}.json`); if (!(await fs.pathExists(dataPath))) continue; const data = await fs.readJson(dataPath); console.log(`📥 Importing ${table}: ${data.length} records`); if (!options.dryRun) { await importTable(table, data, options.mergeStrategy); } } // Import auth sessions const authPath = path.join(importDir, 'auth'); if (await fs.pathExists(authPath)) { console.log('🔐 Importing auth sessions...'); if (!options.dryRun) { await fs.copy(authPath, './data/.wwebjs_auth', { overwrite: options.mergeStrategy === 'replace', }); } } console.log('✅ Import complete'); } ``` ## 14.9 Troubleshooting Migration Issues ### Common Issues | Issue | Cause | Solution | | ------------------------ | ---------------------- | ---------------------------- | | Session not reconnecting | Auth data corrupted | Re-scan QR code | | Foreign key errors | Wrong import order | Use provided import order | | Duplicate key errors | Existing data conflict | Use merge strategy | | Permission denied | File ownership | `chown -R 1000:1000 ./data` | | Out of memory | Large export | Increase Docker memory limit | ### Debug Commands ```bash # Check database integrity sqlite3 ./data/openwa.db "PRAGMA integrity_check;" # Verify auth session files ls -la ./data/.wwebjs_auth/session-*/ # Check file permissions stat ./data/openwa.db stat ./data/.wwebjs_auth # Verify PostgreSQL connection psql $DATABASE_URL -c "SELECT version();" # Check migration status npm run migration:status # Force re-run specific migration npm run migration:run -- --name CreateApiKeysTable ``` ---
[← 13 - Horizontal Scaling Guide](./13-horizontal-scaling.md) · [Documentation Index](./README.md) · [Next: 15 - Project Roadmap →](./15-project-roadmap.md)