# 25. Real-Time Alerts — WebSocket Push Notifications meta: id: kordant-unified-restructure-25 feature: kordant-unified-restructure priority: P1 depends_on: [kordant-unified-restructure-23] tags: [backend, frontend, websocket, realtime, notifications] objective: - Implement real-time alert push from server to client using WebSockets. When a new alert is generated (exposure detected, synthetic voice found, spam blocked, property changed, broker listing found), the user should receive an immediate notification in the web app. deliverables: - `web/src/server/websocket.ts` — WebSocket server: - Integrates with SolidStart's Nitro server or runs alongside it - Authenticates connections using JWT from query param or cookie - Maintains userId → socket mapping - Broadcasts alerts to connected users - `web/src/lib/websocket.ts` — WebSocket client: - Connects to `/ws/alerts` endpoint - Reconnects with exponential backoff on disconnect - Authenticates with JWT token - Exposes `alerts` signal that emits incoming alert objects - Heartbeat/ping-pong to keep connection alive - `web/src/hooks/useRealtimeAlerts.ts` — Hook: - Uses websocket client - Triggers toast notification on new alert - Increments unread badge count in Navbar - Plays subtle notification sound (optional, respects reduced motion) - `web/src/server/services/alert.publisher.ts` — Alert publisher: - `publishAlert(userId, alert)` — sends alert to user's connected sockets - Called from each service's alert pipeline (DarkWatch, VoicePrint, SpamShield, HomeTitle, RemoveBrokers) - Falls back to push notification (task 14) if user is not connected steps: 1. Install `ws` npm package in `web/` (server-side WebSocket library). 2. Create `web/src/server/websocket.ts`: - If using Nitro (SolidStart's server), create a custom Nitro plugin or API route that upgrades to WebSocket - If standalone, create a separate WebSocket server on a different port (e.g., 3001) - On connection: - Parse `token` from query string or cookie - Verify JWT, extract `userId` - Store socket in `userSockets` Map - On message: handle ping with pong, ignore other messages (server pushes only) - On disconnect: remove socket from map - `broadcastToUser(userId, data)`: find all sockets for user, send JSON 3. Create `web/src/lib/websocket.ts`: - `connect()` — create WebSocket, set up event handlers - `disconnect()` — close connection - `onMessage` callback or signal for incoming data - Reconnect logic: 1s, 2s, 5s, 10s, 30s backoff, max 10 attempts - Heartbeat: send ping every 30s, expect pong within 10s 4. Create `web/src/hooks/useRealtimeAlerts.ts`: - Call `connect()` on mount if user is authenticated - On alert received: show toast via `useToast()`, increment unread count - On disconnect: show "Reconnecting..." indicator - Cleanup on unmount 5. Create `web/src/server/services/alert.publisher.ts`: - `publishAlert(userId, alert)`: - Try WebSocket broadcast first - If no active sockets, queue push notification (FCM/APNs) via task 14 - If push fails, queue email notification - `publishToGroup(userIds, alert)` — for family group alerts 6. Integrate with service alert pipelines: - In DarkWatch alert pipeline (task 15): call `alertPublisher.publishAlert(userId, alert)` - In VoicePrint analysis result handler (task 16): same - In SpamShield classification (task 17): same - In HomeTitle change detector (task 18): same - In RemoveBrokers listing scanner (task 19): same 7. Update Navbar: - Add a pulsing dot indicator when WebSocket is connected - Show "Offline" badge when disconnected 8. Write integration tests for WebSocket flow. steps: - Unit: WebSocket server authenticates connections with valid JWT - Unit: WebSocket server rejects connections with invalid JWT - Unit: Client reconnects with exponential backoff on disconnect - Integration: Triggering an alert in DarkWatch service sends real-time notification to connected client - E2E: User receives toast notification within 2 seconds of alert generation acceptance_criteria: - [ ] WebSocket server authenticates connections and maps them to users - [ ] Alerts are broadcast to connected users within 1 second of generation - [ ] Client reconnects automatically after network interruption - [ ] Toast notifications appear for real-time alerts - [ ] Unread badge count increments immediately on new alert - [ ] If user is offline, alert falls back to push notification - [ ] Heartbeat keeps connections alive without timeout - [ ] WebSocket does not leak memory on disconnect/reconnect cycles validation: - Open dashboard in two browser tabs, trigger an alert in one, verify both receive notification - Disconnect network, reconnect, verify client re-establishes connection - Check server memory usage after 100 connect/disconnect cycles - Run `cd web && pnpm test` for WebSocket integration tests notes: - Reference legacy: `server/alerts/` (WebSocket alert server), `packages/api/src/routes/websocket.routes.ts` - Nitro (SolidStart's server) has experimental WebSocket support. If unstable, run a separate `ws` server on a different port and proxy through nginx in production. - For production scaling, consider using Redis Pub/Sub to broadcast alerts across multiple server instances. - The WebSocket connection should be lazy: only connect when user is on a protected route and authenticated. - Respect `prefers-reduced-motion` for notification sounds and aggressive toast animations. - Consider adding a "Do Not Disturb" mode where real-time toasts are suppressed but badge count still updates.