From aura-frog
Guides safe database schema changes and code migrations with zero-downtime strategies including backfills, dual-writes, feature flags, and Strangler Fig pattern.
npx claudepluginhub nguyenthienthanh/aura-frog --plugin aura-frogThis skill uses the workspace's default tool permissions.
**Category:** Dev Expert
Provides best practices for safe database migrations: schema changes, data backfills, rollbacks, zero-downtime deploys for PostgreSQL, MySQL, Prisma, Drizzle, Django.
Plans safe database schema migrations with zero-downtime strategies, rollback procedures, data validation, and version tracking for PostgreSQL, MySQL, and SQLite.
Guides safe schema changes, data migrations, rollbacks, and zero-downtime deployments for PostgreSQL, MySQL, and ORMs like Prisma, Drizzle, Django.
Share bugs, ideas, or general feedback.
Category: Dev Expert Used By: architect, backend agents
Guide safe database and code migrations with zero-downtime strategies.
1. Always backup before migration
2. Test migration on staging first
3. Make migrations reversible
4. Deploy in small batches
5. Monitor during migration
| Change | Safe Approach |
|---|---|
| Add column | Add nullable first, backfill, then NOT NULL |
| Remove column | Stop using → deploy → drop |
| Rename column | Add new → copy data → deploy → drop old |
| Add index | CREATE INDEX CONCURRENTLY |
| Change type | Add new column → migrate → drop old |
Phase 1: Add new column (nullable)
Phase 2: Dual-write (write to both columns)
Phase 3: Backfill old data
Phase 4: Switch reads to new column
Phase 5: Remove old column writes
Phase 6: Drop old column
// migrations/20250128_add_user_status.ts
export async function up(db: Database) {
// 1. Add column
await db.query(`
ALTER TABLE users
ADD COLUMN status VARCHAR(20) DEFAULT 'active'
`)
// 2. Backfill in batches
let lastId = 0
while (true) {
const rows = await db.query(`
UPDATE users SET status = 'active'
WHERE id > $1 AND status IS NULL
LIMIT 1000
RETURNING id
`, [lastId])
if (rows.length === 0) break
lastId = rows[rows.length - 1].id
}
// 3. Add constraint
await db.query(`
ALTER TABLE users
ALTER COLUMN status SET NOT NULL
`)
}
export async function down(db: Database) {
await db.query(`ALTER TABLE users DROP COLUMN status`)
}
// Phase 1: Behind flag
if (featureFlags.newPaymentSystem) {
return newPaymentService.process(order)
}
return oldPaymentService.process(order)
// Phase 2: Gradually increase flag percentage
// Phase 3: Remove flag, delete old code
1. Identify bounded context to migrate
2. Create new implementation alongside old
3. Route traffic gradually to new
4. Remove old when new is stable
Before:
During:
After:
-- Wrong: Locks table, fails on existing rows
ALTER TABLE users ADD COLUMN email VARCHAR NOT NULL;
-- Right: Three-phase approach
ALTER TABLE users ADD COLUMN email VARCHAR;
UPDATE users SET email = 'unknown@example.com' WHERE email IS NULL;
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- Wrong: Blocks writes
CREATE INDEX idx_orders_user ON orders(user_id);
-- Right: Non-blocking
CREATE INDEX CONCURRENTLY idx_orders_user ON orders(user_id);
-- Add new column
ALTER TABLE products ADD COLUMN price_cents INTEGER;
-- Migrate data
UPDATE products SET price_cents = (price * 100)::INTEGER;
-- Switch application to use new column
-- Drop old column
ALTER TABLE products DROP COLUMN price;
| Strategy | When to Use |
|---|---|
| Script rollback | Data changes, can undo |
| Backup restore | Major failure, data loss |
| Feature flag off | Code changes |
| Traffic reroute | Service migration |