Kiến trúc PWA Web Push Notification – Hệ thống VinhPhatERP

Kiến trúc PWA & Web Push Notification – Hệ thống VinhPhatERP

Tài liệu chi tiết về kiến trúc PWA và Web Push Notification đang vận hành trên hệ thống VinhPhatERP, kèm theo phân tích toàn diện các rủi ro kỹ thuật và giải pháp dự phòng.


1. Sơ đồ kiến trúc & luồng dữ liệu (End-to-End)

[1. Hành động người dùng / Sự kiện nghiệp vụ]
       │ (Gửi chat message, duyệt đơn, tạo task, thông báo kho...)
       ▼
[2. PostgreSQL Database (VPS self-hosted)]
       ├── Bước 2.1: Transaction ghi dữ liệu nghiệp vụ
       ├── Bước 2.2: Ghi vào transactional outbox (bảng notification_outbox)
       └── Bước 2.3: DB Trigger kích hoạt HTTP Fast-Path:
             └─► net.http_post() (extension pg_net)
                   │
                   ▼ (gọi nội bộ hoặc qua HTTPS domain VPS)
[3. Supabase Edge Function (send-web-push)]
       ├── Kiểm tra VAPID Key & Auth (Service Role / Anon)
       ├── Idempotency Check (bảng inbound_webhook_events - chống duplicate)
       ├── Data Sanitization (sanitizePushBody - che số tiền nhạy cảm)
       ├── Query danh sách thiết bị nhận (push_subscriptions)
       └── Mã hóa payload chuẩn Web Push RFC 8291 / 8292
             │
             ├──► Apple APNs (push.apple.com với headers APNs) ──┐
             └──► Google FCM / Mozilla Autopush / Microsoft WNS ───┤
                                                                   ▼
[4. Thiết bị Người dùng (iOS PWA / Android / Desktop)]
       ├── Mạng di động / Wifi đẩy tín hiệu về OS
       ├── OS đánh thức Service Worker (sw.js) chạy ngầm
       ├── SW kiểm tra: tab chat có đang mở và focus không?
       │     ├── Nếu đang mở & nhìn thấy: Bỏ qua (không spam)
       │     └── Nếu app đóng / tab ẩn: showNotification() + setAppBadge()
       └── User nhấp vào thông báo:
             └── SW điều hướng / mở URL kèm roomId hoặc entityId

2. Chi tiết 4 tầng kiến trúc

Tầng 1: PWA Client & Quản lý đăng ký thiết bị

File cốt lõi:

  • manifest.json: Định nghĩa chế độ display: standalone, icon maskable, màu theme.
  • usePushSubscription.ts: Hook quản lý vòng đời đăng ký thông báo.
  • pushDeviceInfo.ts: Nhận diện nền tảng (ios, android, windows, safari-pwa), cấp định danh device_id cho từng trình duyệt.

Cơ chế hoạt động:

  • Khi user đăng nhập, hook kiểm tra trạng thái quyền (Notification.permission).
  • Sử dụng VAPID Public Key chính thức (AUTHORITATIVE_PUBLIC_KEY) để gọi reg.pushManager.subscribe().
  • Endpoint cùng 2 khóa mật mã client (p256dh, auth) được lưu vào bảng push_subscriptions.
  • Tự động thu hồi khi đăng xuất: Hàm revokeCurrentDevicePushSubscription() hủy token trên thiết bị dùng chung để tránh rò rỉ tin nhắn cho người đăng nhập sau.

Tầng 2: Database Trigger & Mẫu Transactional Outbox

File cốt lõi:

  • 20260922152000_fix_chat_messages_client_id_and_trigger.sql
  • 20260915000003_notification_outbox_p2_optimization.sql

Cơ chế hoạt động kép (Dual-Path Delivery):

  • Fast Path (Đường hỏa tốc ~1-2s): Khi có tin nhắn, trigger trg_fn_chat_message_inserted() dùng net.http_post() bắn thẳng đến Edge Function send-web-push. User nhận thông báo gần như tức thì.
  • Fallback / Slow Path (Bảo toàn dữ liệu): Đồng thời tin nhắn được ghi vào notification_outbox với trạng thái pending.
  • Cron Worker (pg_cron): Job notification-outbox-processor chạy mỗi 30 giây để quét hàm fn_process_notification_outbox(20). Nếu Fast Path vì lý do mạng/server bị lỗi, sau 15 giây (grace period), Cron sẽ bốc lại tin nhắn để gửi bù và retry tự động (Exponential Backoff).

Tầng 3: Edge Function (send-web-push)

File cốt lõi: supabase/functions/send-web-push/index.ts

Cơ chế hoạt động:

  • Chạy trên Deno runtime trong Docker VPS.
  • Fail Closed: Nếu thiếu VAPID_PRIVATE_KEY hoặc VAPID_PUBLIC_KEY, hàm trả về 503 ngay lập tức để cảnh báo.
  • Fast 202 Response: Trả mã HTTP 202 Accepted cho Postgres trong vòng dưới 30ms, tác vụ đẩy thông báo sang Apple/Google được chạy nền (EdgeRuntime.waitUntil).
  • Chống trùng lặp (Idempotency): Lưu hash dedup_event_id vào inbound_webhook_events. Nếu 1 tin nhắn bị kích hoạt 2 lần, lần thứ 2 sẽ bị bỏ qua (skipped_duplicate).
  • Bảo mật dữ liệu: Hàm sanitizePushBody() tự động che các mẫu số tiền giao dịch (***đ / *** VND) trước khi đẩy lên máy chủ bên thứ ba (APNs/FCM).

Tầng 4: Service Worker & Điều hướng

File cốt lõi: public/sw.js

Cơ chế hoạt động:

  • Bắt sự kiện push: Trích xuất payload JSON.
  • Gọi App Badge API (navigator.setAppBadge(unreadCount)) để hiển thị số chấm đỏ trên icon ứng dụng PWA.
  • Bắt sự kiện notificationclick:
    • Nếu app đang mở sẵn 1 tab: gửi sự kiện postMessage (NAVIGATE_TO_CHAT) để tab đó mở đúng phòng chat và cuộn đến tin nhắn, đồng thời focus cửa sổ.
    • Nếu app đang đóng hoàn toàn: gọi clients.openWindow() mở URL trực tiếp /?chatOpen=1&roomId=...

3. Bảng cảnh báo rủi ro tiềm ẩn & biện pháp phòng ngừa

STT Rủi ro Mức độ Nguyên nhân Hậu quả Giải pháp & Trạng thái hiện tại
1 iOS Safari không nhận được Push (chỉ nhận trên PWA) Cao Apple (iOS 16.4+) áp dụng chính sách: chỉ ứng dụng PWA đã "Thêm vào MH chính" (Add to Home Screen) mới có quyền chạy Web Push nền. Người dùng mở web qua tab Safari thường sẽ không nhận được thông báo khi tắt màn hình; token đăng ký từ tab thường sẽ bị Apple thu hồi ngay. Đã triển khai hàm isIOSNonStandalone(): hệ thống chủ động hiển thị hướng dẫn người dùng iPhone bắt buộc bấm "Thêm vào MH chính" trước khi bật thông báo.
2 Chậm thông báo (latency trễ 30s - 1 phút) Trung bình Fast-path của Postgres (net.http_post) gọi sai URL hoặc bị timeout mạng/firewall, khiến tin nhắn rơi vào hàng đợi outbox chờ pg_cron quét. Tin nhắn không đến tức thì mà mất 30-60 giây mới báo chuông. Đã sửa URL fallback trong DB VPS trỏ chính xác về domain VPS https://quantri.detmayvinhphat.com/functions/v1/send-web-push và cấu hình app.settings.edge_function_url.
3 Container Edge Function mất biến môi trường VAPID Cao Khi rebuild hoặc restart container supabase-edge-functions trên VPS mà file .env của Docker thiếu VAPID_PRIVATE_KEY. Toàn bộ push notification bị lỗi 503, không thiết bị nào nhận được thông báo. Đã có cơ chế Fail-Closed cảnh báo rõ ràng. Cần đảm bảo file .env tại thư mục docker Supabase trên VPS luôn chứa cặp key VAPID chuẩn.
4 Rác token trong database (push_subscriptions) Thấp Người dùng gỡ app, xóa cache trình duyệt, nâng cấp iOS hoặc đổi máy mà không bấm nút tắt thông báo. Bảng subscription chứa hàng nghìn endpoint chết; Edge Function mất thời gian gửi vô ích sang Apple/Google. Đã triển khai Strict Error Classification: khi APNs/FCM trả về 404, 410 (Gone) hoặc BadDeviceToken, Edge Function tự động đánh dấu revoked_at = NOW() để loại bỏ vĩnh viễn.
5 Apple APNs chặn thông báo do thiếu header đặc thù Trung bình Apple APNs khắt khe hơn Google FCM: yêu cầu bắt buộc các headers như apns-push-type: alert, apns-priority: 10, apns-expiration. Người dùng iPhone không nhận được thông báo khi máy khóa màn hình hoặc ở chế độ tiết kiệm pin. Đã cấu hình chuyên biệt trong send-web-push/index.ts nhận diện endpoint push.apple.com để bổ sung đầy đủ header APNs.
6 Tràn dung lượng bộ nhớ bảng net._http_response Thấp Extension pg_net của Postgres lưu lại lịch sử phản hồi của mọi request HTTP push. Nếu mỗi ngày có hàng chục nghìn lượt chat, bảng này sẽ phình to. Database bị chiếm dụng ổ cứng và làm chậm hiệu năng I/O. Đã thiết lập job pg_net-response-purge chạy lúc 03:30 UTC hàng ngày tự động xóa các log HTTP cũ hơn 3 ngày.

4. Hướng dẫn vận hành & khuyến nghị cho người dùng

Đối với nhân viên dùng iPhone (iOS)

  1. Truy cập https://quantri.detmayvinhphat.com bằng trình duyệt Safari.
  2. Nhấn nút Chia sẻ (Share) ở thanh công cụ Safari, chọn "Thêm vào MH chính" (Add to Home Screen).
  3. Thoát Safari, mở ứng dụng từ biểu tượng VP ERP trên màn hình chính, đăng nhập và nhấn Bật thông báo.

Đối với quản trị hệ thống (DevOps / VPS)

Nếu có nâng cấp cấu hình Docker Supabase trên VPS 103.213.216.31, luôn kiểm tra container supabase-edge-functions đã nhận đủ VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT; đồng thời đảm bảo database setting app.settings.edge_function_url luôn trỏ tới đúng domain hoạt động của VPS.

Đăng nhận xét

Mới hơn Cũ hơn