Moving a Django production app from Supabase to AWS RDS takes roughly 4–5 hours of setup and a 1–2 hour maintenance window. This guide covers every phase — from database creation to decommissioning Supabase after 30 days of stable operation.
Estimated Timeline
Phase
Duration
Notes
RDS Setup
2–3 hours
AWS console, security groups, parameter groups
Pre-Migration Prep
1 hour
Backups, documentation
Schema Migration
30 min
Run Django migrations on RDS
Maintenance Window
1–2 hours
Actual cutover
— Data dump
20 min
From Supabase
— Data restore
30 min
To RDS
— Verification
20 min
Row counts, integrity checks
— Config update + deploy
30 min
Env vars, smoke tests
Post-migration monitoring
24 hours
Intensive watch
Optimization
Ongoing
7–30 days
Total actual downtime: 1–2 hours (maintenance window only)
Connect to your RDS instance as the master user and run:
-- Create database
CREATE DATABASE db_name WITH
ENCODING 'UTF8'
LC_COLLATE 'en_US.UTF-8'
LC_CTYPE 'en_US.UTF-8';
-- Create application user
CREATE USER db_user WITH PASSWORD 'your_secure_password_here';
-- Grant database privileges
GRANT ALL PRIVILEGES ON DATABASE db_name TO db_user;
-- Switch to new database
\c db_name
-- Grant schema privileges
GRANT ALL ON SCHEMA public TO db_user;
GRANT ALL ON ALL TABLES IN SCHEMA public TO db_user;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO db_user;
-- Ensure future objects are also granted
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO db_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO db_user;
-- Install extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public'
LOOP
EXECUTE 'ALTER TABLE ' || quote_ident(r.tablename) || ' ENABLE TRIGGER ALL';
END LOOP;
END $$;
Reset all sequences to current max values:
DO $$
DECLARE
r RECORD;
max_id BIGINT;
BEGIN
FOR r IN
SELECT
schemaname,
tablename,
pg_get_serial_sequence(schemaname||'.'||tablename, 'id') as seqname
FROM pg_tables
WHERE schemaname = 'public'
AND pg_get_serial_sequence(schemaname||'.'||tablename, 'id') IS NOT NULL
LOOP
EXECUTE 'SELECT COALESCE(MAX(id), 0) + 1 FROM '
|| quote_ident(r.schemaname) || '.' || quote_ident(r.tablename) INTO max_id;
EXECUTE 'ALTER SEQUENCE ' || r.seqname || ' RESTART WITH ' || max_id;
END LOOP;
END $$;
Update query planner statistics:
VACUUM ANALYZE;
Phase 5 — Data Verification
Row Count Check
Run on both Supabase and RDS, then compare:
-- Full table row counts
SELECT
schemaname,
tablename,
n_live_tup as row_count
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;
-- Verify FK constraints exist
SELECT conname, conrelid::regclass, confrelid::regclass
FROM pg_constraint
WHERE contype = 'f'
AND connamespace = 'public'::regnamespace;
-- Check for orphaned records (should return 0)
<SQL Query for JOINS>
Index Verification
SELECT
schemaname,
tablename,
COUNT(*) as index_count
FROM pg_indexes
WHERE schemaname = 'public'
GROUP BY schemaname, tablename
ORDER BY tablename;
Phase 6 — Application Configuration
Environment Variables
Remove Supabase variables:
# Remove these
SUPABASE_HOST
SUPABASE_PORT
SUPABASE_DB_NAME
SUPABASE_DB_USER
SUPABASE_DB_PASSWORD
DATABASE_POOLED_URL
Add RDS variables:
DB_HOST=org-production.xxxxx.ap-southeast-1.rds.amazonaws.com
DB_PORT=5432
DB_NAME=db_name
DB_USER=db_user
DB_PASSWORD=your_secure_password
# Full URL form
DATABASE_URL=postgresql://db_user:password@org-production.xxxxx.rds.amazonaws.com:5432/db_name
# If using RDS Proxy
DB_HOST=org-proxy.proxy-xxxxx.ap-southeast-1.rds.amazonaws.com
Django Settings
No code changes needed — update via env vars only:
# manage.py shell
from django.db import connection
cursor = connection.cursor()
cursor.execute("SELECT COUNT(*) FROM sales_saleorder")
print(cursor.fetchone())
Django Test Suite
python manage.py test
python manage.py test sales
python manage.py test product
python manage.py test forecasting
python manage.py test integrations.unicommerce
Performance Baseline
\timing on
EXPLAIN ANALYZE SELECT * FROM sales_saleorder WHERE status = 'pending' LIMIT 100;
EXPLAIN ANALYZE SELECT * FROM product_productvariant WHERE product_id = 1;
Manual Checklist
User authentication (login / logout)
Create and view sale orders
Product variant queries
Forecasting queries
Unicommerce sync
API endpoints (Postman collection)
Admin panel access
Phase 8 — Cutover (Maintenance Window)
Pre-Cutover Checklist
RDS running and accessible
Data migrated and verified
Application tested against RDS
Supabase backup saved to S3
Env vars prepared (not yet deployed)
Rollback plan documented
Stakeholders notified
CloudWatch dashboards open
Cutover Steps
T-0: Enable maintenance mode
vercel env add MAINTENANCE_MODE true production
T-5: Stop background jobsStop Celery workers, cron jobs, and incoming webhooks.T-10: Final incremental sync
T-60+: MonitorWatch application logs, RDS CloudWatch metrics, error rates, and API response times.
CloudWatch Alarms to Set
Metric
Threshold
CPU Utilization
> 80%
DatabaseConnections
> 180 (of 200 max)
FreeStorageSpace
< 10 GB
ReadLatency
> 100ms
WriteLatency
> 100ms
Phase 9 — Rollback Plan
Immediate Rollback (< 1 hour after cutover)
# Revert env vars to Supabase
vercel env add DB_HOST aws-1-ap-southeast-1.pooler.supabase.com production
# Restore all other Supabase DB vars
vercel --prod
Then verify Supabase connectivity and re-enable traffic.
Extended Rollback (> 1 hour after cutover)
If data has been written to RDS during the window:
-- On RDS: check scope of new data
SELECT COUNT(*) FROM sales_saleorder WHERE created_at > '2026-03-21 10:00:00';
Decision criteria for rollback:
Critical bug preventing operation
Data corruption detected
Performance issues blocking users
If counts are low, export new RDS data and merge back to Supabase. If counts are high, troubleshoot on RDS rather than rolling back.
Phase 10 — Post-Migration Tasks
7-Day Review
-- Enable slow query logging
ALTER SYSTEM SET log_min_duration_statement = 1000;
Review CloudWatch Logs for slow queries, then add indexes where needed. Tune parameter group based on actual workload patterns.