Thay thế Supabase Realtime bằng Socket.IO hoặc Pusher: hướng dẫn thực chiến
Supabase Realtime mạnh khi cần nghe thay đổi từ Postgres nhanh, ít cấu hình, hợp MVP. Nhưng khi sản phẩm lớn hơn, nhu cầu thường đổi: cần kiểm soát kênh tốt hơn, giảm tải database, gom sự kiện nghiệp vụ, scale theo kiểu riêng, hoặc chạy đa nền tảng với độ trễ ổn định hơn. Lúc đó, thay Supabase Realtime bằng Socket.IO hoặc Pusher là lựa chọn đáng cân nhắc.
Bài này đi thẳng vào thực chiến: khi nào nên thay, chọn Socket.IO hay Pusher, kiến trúc nên dùng, cách migrate từng bước, và lỗi hay gặp.
Vì sao thay Supabase Realtime?
Supabase Realtime nghe thay đổi qua replication/Postgres changes. Tiện, nhưng có vài giới hạn thực tế.
Database thành nguồn phát event
Khi client subscribe trực tiếp vào thay đổi bảng, database không chỉ lưu dữ liệu mà còn thành “event broker”. Với app nhỏ ổn. Với app nhiều người dùng, nhiều bảng, nhiều filter, chi phí và độ phức tạp tăng.
Ví dụ: chat, notification, live dashboard, presence. Không phải thay đổi nào trong DB cũng nên bắn ra client. Nhiều event nên là event nghiệp vụ, không phải row-level change.
Khó kiểm soát quyền theo ngữ cảnh
Supabase có RLS rất mạnh. Nhưng realtime permission có thể khó debug khi rule phức tạp: theo team, role, trạng thái invoice, project membership, tenant.
Nếu chuyển sang backend tự phát event, ta có thể kiểm tra quyền trong service layer rồi emit đúng người, đúng room.
Cần pattern realtime khác
Một số case cần:
– Presence: ai đang online, đang gõ, đang xem document.
– Typing indicator: event ngắn hạn, không cần ghi DB.
– Job progress: trạng thái xử lý file, AI task, import CSV.
– Notification fanout: gửi nhiều user, nhiều device.
– Game/livestream interaction: tần suất cao, không phù hợp DB-trigger realtime.
Những thứ này hợp WebSocket/event broker hơn Postgres changes.
Socket.IO hay Pusher?
Socket.IO: kiểm soát cao, tự vận hành
Socket.IO chạy trên server của bạn. Có fallback transport, room, namespace, middleware auth, adapter Redis để scale ngang.
Hợp khi:
– Có backend Node.js/NestJS/Express.
– Muốn kiểm soát protocol, auth, room, payload.
– Muốn tự host để giảm chi phí dài hạn.
– Cần event tần suất cao.
– Có hạ tầng Redis, load balancer, observability.
Đổi lại:
– Phải vận hành server realtime.
– Phải xử lý scale, reconnect, monitoring.
– Cần cấu hình sticky session hoặc Redis adapter đúng.
Pusher: dịch vụ managed, triển khai nhanh
Pusher Channels cung cấp WebSocket managed. Server chỉ gọi API trigger event. Client subscribe channel.
Hợp khi:
– Muốn đi nhanh, ít vận hành.
– Team nhỏ, không muốn quản lý WebSocket server.
– App cần realtime vừa phải: notification, dashboard, chat.
– Cần uptime ổn, dashboard sẵn.
Đổi lại:
– Chi phí tăng theo connection/message.
– Bị lock-in API/channel model.
– Ít kiểm soát tầng transport hơn Socket.IO.
Kiến trúc thay thế nên dùng
Mục tiêu: không để client nghe database trực tiếp nữa. Backend thành nơi tạo event.
Luồng chuẩn
1. Client gọi API mutation: tạo message, cập nhật task, đổi status.
2. Backend validate auth và business rule.
3. Backend ghi DB.
4. Backend emit event qua Socket.IO hoặc Pusher.
5. Client nhận event, cập nhật UI cache.
Không có adapter, emit từ instance A không tới socket đang nằm ở instance B.
Load balancer
Cần hỗ trợ WebSocket upgrade. Với Socket.IO, nên bật sticky session nếu dùng polling fallback. Nếu chỉ dùng websocket transport, yêu cầu sticky giảm nhưng vẫn cần test kỹ.
Vấn đề thực tế: DB ghi xong, server crash trước khi emit. Client không nhận event.
Giải pháp chắc hơn: outbox table.
1. Trong cùng transaction, ghi business data và ghi event vào event_outbox.
2. Worker đọc outbox chưa gửi.
3. Worker emit qua Socket.IO/Pusher.
4. Đánh dấu sent_at.
Schema tối giản:
create table event_outbox (
id uuid primary key,
event_name text not null,
channel text not null,
payload jsonb not null,
created_at timestamptz default now(),
sent_at timestamptz
);
Pattern này tăng độ tin cậy, nhất là với payment, notification quan trọng, workflow nhiều bước.
Lỗi hay gặp
Emit trước khi commit
Nếu emit trước khi transaction commit, client fetch lại có thể chưa thấy dữ liệu. Luôn emit sau commit, hoặc dùng outbox.
Trust client quá mức
Client không được tự quyết join project:123. Server phải kiểm tra quyền.
Payload phụ thuộc DB schema
Nếu rename column là client vỡ. Dùng event contract ổn định.
Không xử lý reconnect
Client mất mạng rồi kết nối lại có thể miss event. Khi reconnect, nên refetch dữ liệu quan trọng:
Realtime chỉ là lớp đồng bộ nhanh, không nên là nguồn sự thật duy nhất.
Kết luận thực tế
Supabase Realtime tốt cho khởi đầu, nhưng không phải lúc nào cũng là nền tảng realtime dài hạn. Nếu cần kiểm soát sâu, event tần suất cao, self-host, chọn Socket.IO. Nếu muốn triển khai nhanh, ít vận hành, trả tiền để lấy sự ổn định, chọn Pusher.
Cách migrate an toàn: đổi từ row-level change sang event nghiệp vụ, đưa mutation về backend, thiết kế channel rõ, kiểm tra quyền khi subscribe, chạy song song, rồi tắt Supabase Realtime từng module. Với hệ thống quan trọng, thêm outbox pattern để không mất event.
Realtime tốt không chỉ là “bắn event nhanh”. Realtime tốt là đúng quyền, đúng thời điểm, không mất dữ liệu, scale được, và client vẫn hồi phục tốt khi mạng xấu.