Cách migrate dữ liệu từ Supabase sang Neon PostgreSQL không downtime
Migrate dữ liệu từ Supabase sang Neon PostgreSQL nghe có vẻ “chỉ là đổi connection string”. Thực tế, rủi ro nằm ở phần sống: app vẫn ghi dữ liệu, user vẫn tạo đơn, webhook vẫn chạy, job nền vẫn update record. Nếu dừng hệ thống để dump rồi restore, dễ làm. Nếu không downtime, cần kế hoạch khác: đồng bộ dữ liệu ban đầu, bắt kịp thay đổi mới, kiểm tra toàn vẹn, rồi cutover nhanh.
Supabase dùng PostgreSQL. Neon cũng là PostgreSQL serverless. Lợi thế lớn: có thể dùng công cụ chuẩn như pg_dump, pg_restore, logical replication, hoặc CDC. Nhưng Supabase thêm nhiều phần riêng: Auth, Storage, Realtime, Edge Functions, RLS policy. Vì vậy migrate phải tách rõ: database, auth, storage, app config, secret, job, realtime.
Bài này tập trung vào migrate database PostgreSQL từ Supabase sang Neon với downtime gần 0, kèm checklist thực tế.
Hiểu đúng “không downtime”
Không downtime không có nghĩa không có giây nào thay đổi. Thường nghĩa là:
– App vẫn đọc/ghi trong quá trình copy dữ liệu.
– Cutover chỉ đổi database URL, redeploy nhanh, hoặc switch runtime config.
– Mất kết nối rất ngắn, vài giây đến vài chục giây.
– Không mất dữ liệu đã ghi ở Supabase trước cutover.
– Có plan rollback nếu Neon gặp lỗi.
Muốn làm được, cần xử lý 2 pha:
1. Initial load: copy dữ liệu hiện có từ Supabase sang Neon.
2. Change sync: đồng bộ thay đổi phát sinh trong lúc app vẫn chạy.
Nếu chỉ dùng pg_dump rồi restore, dữ liệu sau thời điểm dump sẽ bị thiếu. Vì vậy cần logical replication hoặc cơ chế dual-write tạm thời.
Chuẩn bị trước migration
Kiểm tra phiên bản PostgreSQL
Supabase và Neon đều chạy PostgreSQL, nhưng version có thể khác. Kiểm tra:
select version();
Nên chọn Neon version bằng hoặc cao hơn Supabase. Tránh dùng extension chưa được Neon hỗ trợ.
Liệt kê extension
select extname, extversion
from pg_extension
order by extname;
App thường dùng public hoặc schema riêng. Không nên dump mù toàn database rồi restore sang Neon, vì có object Supabase-managed không chạy được hoặc không cần.
Xác định schema cần migrate:
select schema_name
from information_schema.schemata
order by schema_name;
Audit RLS, trigger, function
RLS policy quan trọng nếu app dùng Supabase client trực tiếp từ frontend. Kiểm tra:
select schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
from pg_policies
order by schemaname, tablename;
Trigger và function cũng cần dump:
select trigger_schema, event_object_table, trigger_name
from information_schema.triggers
order by trigger_schema, event_object_table;
Chiến lược migration không downtime
Có 3 hướng chính.
Hướng 1: Logical replication từ Supabase sang Neon
Đây là hướng sạch nếu Supabase project cho phép logical replication và quyền đủ. Luồng:
– Supabase làm publisher.
– Neon làm subscriber.
– Copy dữ liệu ban đầu qua replication.
– Replication tiếp tục stream thay đổi.
– Khi lag = 0, cutover app sang Neon.
Ưu điểm:
– Không cần dual-write trong app.
– Ít rủi ro mất dữ liệu.
– Hợp PostgreSQL-native.
Nhược điểm:
– Cần quyền tạo publication, replication slot.
– Một số managed platform giới hạn quyền.
– DDL không tự replicate. Schema change phải quản lý riêng.
Tạo publication trên Supabase:
create publication neon_migration_pub for all tables;
Hoặc chỉ bảng app:
create publication neon_migration_pub
for table public.users, public.orders, public.payments;
Nếu logical replication không khả dụng, dùng dump/restore cho dữ liệu ban đầu, rồi ghi lại thay đổi sau mốc dump.
Cách đơn giản:
– Tạo bảng changelog trong Supabase.
– Trigger trên bảng quan trọng ghi insert/update/delete.
– Dump dữ liệu.
– Restore sang Neon.
– Replay changelog sang Neon.
– Dừng ghi ngắn, replay phần cuối, cutover.
Ví dụ changelog:
create table public.migration_changelog (
id bigserial primary key,
table_name text not null,
op text not null,
row_pk jsonb not null,
row_data jsonb,
created_at timestamptz default now()
);
Trigger cần viết kỹ cho từng bảng hoặc generic bằng to_jsonb(NEW) / to_jsonb(OLD).
– Tự viết replay logic.
– Dễ lỗi với delete, update partial, conflict.
– Cần test mạnh.
Hướng 3: Dual-write tạm thời trong application
App ghi cả Supabase và Neon trong giai đoạn migration.
Luồng:
– Dump/restore dữ liệu cũ.
– Bật dual-write cho write path.
– So sánh dữ liệu.
– Chuyển read sang Neon.
– Tắt ghi Supabase.
Ưu điểm:
– Không cần quyền replication.
– Hợp nếu app có service layer rõ.
Nhược điểm:
– Phức tạp với transaction.
– Dễ lệch khi một bên fail.
– Cần idempotency key và retry queue.
Nếu app hiện ghi trực tiếp từ frontend vào Supabase bằng anon key, dual-write khó hơn nhiều. Khi đó nên chuyển write qua backend/API trước.
Quy trình đề xuất: logical replication + cutover nhanh
Bước 1: Tạo Neon project và role
Tạo database trên Neon. Bật extension cần thiết:
create extension if not exists pgcrypto;
create extension if not exists "uuid-ossp";
Tạo role app:
create role app_user with login password 'STRONG_PASSWORD';
grant connect on database neondb to app_user;
grant usage on schema public to app_user;
grant select, insert, update, delete on all tables in schema public to app_user;
grant usage, select on all sequences in schema public to app_user;
Set default privilege:
alter default privileges in schema public
grant select, insert, update, delete on tables to app_user;
alter default privileges in schema public
grant usage, select on sequences to app_user;
Nếu có function dùng auth.uid() của Supabase, Neon không có sẵn. Cần thay bằng cơ chế auth của app, hoặc tạo compatibility function nếu phù hợp. Đây là điểm hay gây lỗi.
Bước 3: Bật logical replication
Trên Supabase:
create publication neon_migration_pub for all tables;
Nếu lỗi do bảng thiếu primary key, replication update/delete có thể fail hoặc không đủ identity. Kiểm tra:
select table_schema, table_name
from information_schema.tables t
where table_schema = 'public'
and table_type = 'BASE TABLE'
and not exists (
select 1
from information_schema.table_constraints c
where c.table_schema = t.table_schema
and c.table_name = t.table_name
and c.constraint_type = 'PRIMARY KEY'
);
Bảng có update/delete nên có primary key. Nếu không, thêm key hoặc set replica identity:
alter table public.some_table replica identity full;
Bước 4: Theo dõi sync và kiểm tra dữ liệu
Kiểm tra subscription:
select subname, pid, received_lsn, latest_end_lsn, latest_end_time
from pg_stat_subscription;
So sánh count:
select count(*) from public.orders;
So sánh checksum mức bảng:
select md5(string_agg(md5(t::text), '' order by id))
from public.orders t;
Với bảng lớn, đừng checksum toàn bảng giờ cao điểm. Chia theo range ID hoặc thời gian.
Kiểm tra sequence sau restore/replication:
select setval(
pg_get_serial_sequence('public.orders', 'id'),
(select max(id) from public.orders)
);
Sequence lệch gây lỗi duplicate key sau cutover.
Bước 5: Chuẩn bị cutover
Trước cutover:
– Freeze deploy có schema migration.
– Tắt job nền ghi dữ liệu nếu có thể.
– Đảm bảo replication lag = 0.
– Kiểm tra connection pool config.
– Chuẩn bị env var Neon.
– Chuẩn bị rollback env var Supabase.
– Backup Supabase trước chuyển.
Nếu app có migration tool như Prisma, Drizzle, Rails migration, phải đảm bảo schema Neon giống production Supabase. Không để app tự chạy migration bất ngờ lúc boot.
Bước 6: Cutover
Thứ tự an toàn:
1. Bật maintenance nhẹ cho write path nếu chấp nhận được vài giây. Read vẫn chạy.
2. Dừng worker/job ghi vào Supabase.
3. Đợi replication bắt kịp.
4. Chuyển DATABASE_URL sang Neon.
5. Redeploy app hoặc reload config.
6. Smoke test: login, tạo record, update, delete, query chính.
7. Mở write path lại.
Nếu không muốn maintenance, cần đảm bảo mọi write sau thời điểm cutover đi Neon. Load balancer và instance cũ không được giữ connection Supabase quá lâu.
Supabase Auth, Storage, Realtime: xử lý sao?
Auth
Supabase Auth lưu trong schema auth. Neon không cung cấp Supabase Auth managed. Có 3 lựa chọn:
– Giữ Supabase Auth, chỉ chuyển app database sang Neon.
– Migrate sang provider khác như Clerk, Auth0, Better Auth, custom auth.
– Tự host Supabase Auth stack. Khó hơn.
Nếu giữ Supabase Auth, app cần xác minh JWT Supabase rồi map user_id vào Neon. RLS phụ thuộc auth.uid() cần thiết kế lại.
Storage
Supabase Storage không tự chuyển sang Neon vì Neon là database, không phải object storage. Chuyển file sang S3/R2/GCS nếu muốn rời Supabase Storage. Metadata trong storage.objects có thể migrate, nhưng binary object cần copy riêng.
Realtime
Supabase Realtime phụ thuộc publication và service riêng. Neon không thay thế trực tiếp. Cần dùng Ably, Pusher, Socket.IO, hoặc polling/listen-notify qua backend.
Lỗi thường gặp
Thiếu extension
Restore báo function/type không tồn tại. Fix: tạo extension trước hoặc bỏ dependency.
RLS policy dùng Supabase function
Policy như:
using (auth.uid() = user_id)
Sang Neon sẽ lỗi nếu schema auth hoặc function uid() không tồn tại. Fix bằng backend authorization hoặc function tương thích.
DDL không replicate
Logical replication không sync alter table, create index, create function. Trong migration window, cấm schema change hoặc apply tay trên cả hai DB.
Sequence không đúng
Insert mới lỗi duplicate key. Fix bằng setval() cho sequence sau sync.
Connection limit
Neon serverless có pooling riêng. App serverless tạo nhiều connection có thể vượt giới hạn. Dùng pooled connection string hoặc PgBouncer-compatible mode nếu framework phù hợp.
Checklist rollback
Rollback phải có trước cutover, không phải sau lỗi mới nghĩ.
– Giữ Supabase database nguyên trạng.
– Không drop publication ngay.
– Log thời điểm cutover.
– Nếu Neon lỗi ngay sau cutover, đổi DATABASE_URL về Supabase.
– Nếu đã có write mới ở Neon, rollback phức tạp hơn. Cần sync ngược hoặc chấp nhận mất/merge thủ công.
– Với hệ thống quan trọng, bật dual-write ngắn sau cutover để dễ rollback.
Kết luận thực tế
Migrate Supabase sang Neon PostgreSQL không downtime làm được, nhưng không nên coi là thao tác dump/restore. Cách tốt nhất thường là: dump schema, dùng logical replication để copy và sync data, kiểm tra toàn vẹn, cutover nhanh, giữ rollback rõ.
Phần database tương đối thẳng vì cả hai đều là PostgreSQL. Phần khó nằm ở thứ Supabase cung cấp ngoài database: Auth, Storage, Realtime, RLS helper, edge workflow. Trước khi migrate, hãy phân loại rõ cái nào chuyển sang Neon, cái nào giữ ở Supabase, cái nào thay bằng dịch vụ khác.
Migration tốt không phải migration không có rủi ro. Migration tốt là migration có kiểm soát, đo được lag, test được dữ liệu, rollback được khi sai, và user gần như không biết chuyện gì vừa xảy ra.