35 KiB
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
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
## 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
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:
# 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 postgres 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
Export Response Example:
{
"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:
# 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/exports/storage-export-xxx.tar.gz" }
# The archive is auto-removed after STORAGE_EXPORT_TTL_MS (default 1h), so re-import it before then.
# It is written under data/ so it survives the restart in Step 4 and stays import-able.
# 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/exports/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:
# 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_USERNAME=optional
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:
# 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/queuesdashboard.
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)
// 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<MigrationResult[]> {
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<MigrationResult> {
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<void> {
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<string[]> {
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
# 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
-- 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)
#!/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 <session-id>"
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
// 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<SessionExport> {
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<Session> {
// 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
// scripts/bulk-transfer-sessions.ts
interface TransferConfig {
sourceUrl: string;
sourceApiKey: string;
targetUrl: string;
targetApiKey: string;
sessionIds: string[] | 'all';
}
async function bulkTransferSessions(config: TransferConfig): Promise<void> {
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
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
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
#!/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:prod # the prod image strips ts-node/TS source — use :prod
# 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
breaking_changes:
api:
- Groups API restructured
- Webhook payload format v2
- Rate limiting enforced
config:
- ENGINE_TYPE required (default: whatsapp-web.js; also accepts: baileys)
- STORAGE_ADAPTER required (default: local)
database:
- New tables: groups, group_participants, channels
- sessions.status enum expanded
Migration Steps
#!/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 # default (Chromium-based); set to "baileys" for browser-free engine
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)
#!/bin/bash
# rollback.sh
BACKUP_DIR=$1
TARGET_VERSION=$2
if [ -z "$BACKUP_DIR" ] || [ -z "$TARGET_VERSION" ]; then
echo "Usage: ./rollback.sh <backup-dir> <target-version>"
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
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
# 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
# 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
// scripts/full-export.ts
interface ExportOptions {
outputDir: string;
includeMedia: boolean;
includeLogs: boolean;
compress: boolean;
}
async function fullExport(options: ExportOptions): Promise<void> {
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
// scripts/full-import.ts
interface ImportOptions {
inputPath: string;
mergeStrategy: 'replace' | 'merge' | 'skip-existing';
dryRun: boolean;
}
async function fullImport(options: ImportOptions): Promise<void> {
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 |
PostgreSQL: boot crash-loop after upgrading a DATABASE_SYNCHRONIZE=true deployment
Symptom: after upgrade, the container crash-loops on boot. docker logs shows one of:
column "id" is of type uuid but default expression is of type character varyingforeign key constraint ... cannot be implemented ... incompatible types: character varying and uuid
Cause: a deployment previously bootstrapped with DATABASE_SYNCHRONIZE=true on PostgreSQL has native uuid id/FK columns (TypeORM derives them from @PrimaryGeneratedColumn('uuid')), while the migration chain assumes varchar. The two are incompatible, and migrations run unconditionally on the Postgres data connection (migrationsRun: true), so boot cannot complete (issue #690).
Fix (automatic for most deployments): OpenWA ships a guard migration (NormalizeSynchronizeUuidColumns, ordered before the first collision) that converts the affected uuid columns to varchar on the next boot. For small-to-medium databases this is transparent — upgrade and restart.
Large-database maintenance window: the conversion rewrites messages and message_batches in full under an exclusive lock. If either table is large (millions of rows) and your orchestrator's liveness/readiness grace is tight, run the migration against the stopped app during a planned window:
docker compose down
DATABASE_TYPE=postgres DATABASE_HOST=... DATABASE_USERNAME=... \
DATABASE_PASSWORD=... DATABASE_NAME=openwa npm run migration:run
docker compose up -d
(The CLI runner does not impose a statement timeout; the migration lifts it via SET LOCAL.)
DATABASE_SYNCHRONIZE=true on PostgreSQL is unsupported for production. Leave it unset (the default false) and let migrations manage the schema.
Debug Commands
# 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