8.6 KiB
phase: 26-pwa-performance plan: "04" subsystem: api, database, ui tags: [push-notifications, web-push, vapid, service-worker, pwa, drizzle, postgres]
requires:
- phase: 26-00 provides: Service worker push event handler and idb infrastructure
- phase: 26-02 provides: MobileChatView and responsive layout modifications to ChatPanel
- phase: 26-03 provides: InstallPromptBanner and OfflineBanner already rendered in ChatPanel
provides:
- push_subscriptions PostgreSQL table via pgTable drizzle schema
- 0055_create_push_subscriptions.sql migration
- pushService with initVapid, saveSubscription, removeSubscription, sendPushToAll
- pushRoutes mounted at /api/push (GET vapid-public-key, POST/DELETE subscribe)
- ui/src/api/push.ts client API module
- ui/src/hooks/usePushNotifications.ts subscription hook with SW pushManager
- ui/src/components/NotificationPermissionPrompt.tsx with engagement gate
affects:
- Any future phase triggering push notifications on agent events
- Plan 26-05 (final PWA plan, if any)
tech-stack: added: - web-push 3.x (server VAPID + push protocol) - "@types/web-push" (TypeScript types) - idb 8.x (installed for UI to fix missing 26-00 dependency) patterns: - pushService functions (not class) matching existing service pattern - Graceful VAPID skip when env vars not configured - Auto-delete stale 410/404 push subscriptions on send failure - Engagement gate (agentResponseCount >= 3) before showing permission prompt - localStorage nexus.notifPromptDismissed for dismiss persistence
key-files: created: - packages/db/src/schema/push_subscriptions.ts - packages/db/src/migrations/0055_create_push_subscriptions.sql - server/src/services/pushService.ts - server/src/routes/push.ts - ui/src/api/push.ts - ui/src/hooks/usePushNotifications.ts - ui/src/components/NotificationPermissionPrompt.tsx modified: - packages/db/src/schema/index.ts - server/src/app.ts - ui/src/components/ChatPanel.tsx - pnpm-lock.yaml
key-decisions:
- "pushService uses named exports (not class) matching existing chat.ts service pattern"
- "initVapid is graceful — checks env vars before calling setVapidDetails, logs warning if absent"
- "sendPushToAll uses Promise.allSettled so one failed delivery doesn't block others"
- "Stale subscriptions auto-deleted on 410/404 response per RESEARCH Pitfall 6"
- "DELETE /api/push/subscribe uses request body (not URL param) since endpoints can be long URLs"
- "ui/src/api/push.ts uses direct fetch for DELETE with body — api client only supports parameterless DELETE"
- "agentResponseCount derived via useMemo from messages array with role === assistant filter"
- "urlBase64ToUint8Array uses .buffer as ArrayBuffer cast for TypeScript strict mode compatibility"
- "idb installed as explicit ui dependency — was missing from 26-00 causing build failure"
patterns-established:
- "Push routes pattern: pushRoutes(db) returning Express Router, mounted at /api/push"
- "VAPID init at app startup, wrapped in try/catch for graceful degradation"
- "Notification engagement gate: check agentResponseCount >= 3 before showing prompt"
requirements-completed:
- PWA-06
duration: 15min completed: 2026-04-02
Phase 26 Plan 04: Push Notifications Summary
End-to-end web push notifications: PostgreSQL push_subscriptions table, VAPID server service, /api/push routes, SW pushManager subscription hook, and engagement-gated permission prompt
Performance
- Duration: ~15 min
- Started: 2026-04-02T02:27:38Z
- Completed: 2026-04-02T02:42:00Z
- Tasks: 2
- Files modified: 10
Accomplishments
- Push subscription DB table with pg-core schema (uuid, text, timestamp with timezone), index on endpoint
- Server-side VAPID management via web-push library — initVapid() called at startup, graceful skip if env vars absent
- Three push API routes: GET vapid-public-key, POST subscribe, DELETE subscribe — stale 410/404 endpoints auto-cleaned
- Client hook (usePushNotifications) handles permission request + SW pushManager.subscribe + server sync
- NotificationPermissionPrompt renders after 3rd agent response, respects localStorage dismiss state
- Fixed pre-existing missing
idbdependency that was blocking UI build
Task Commits
- Task 1: Backend — schema, migration, pushService, push routes -
ad4cc035(feat) - Task 2: Frontend — push API client, hook, prompt, ChatPanel update -
57d7a730(feat)
Files Created/Modified
packages/db/src/schema/push_subscriptions.ts- pgTable with endpoint, p256dh, auth, userId, companyId, deviceLabelpackages/db/src/migrations/0055_create_push_subscriptions.sql- CREATE TABLE + endpoint indexpackages/db/src/schema/index.ts- Added export for pushSubscriptionsserver/src/services/pushService.ts- initVapid, getVapidPublicKey, saveSubscription, removeSubscription, sendPushToAllserver/src/routes/push.ts- Express Router with GET /vapid-public-key, POST /subscribe, DELETE /subscribeserver/src/app.ts- Import + mount pushRoutes at /api/push, call initVapid() at startupui/src/api/push.ts- pushApi client with getVapidPublicKey, subscribe, unsubscribe methodsui/src/hooks/usePushNotifications.ts- Hook with urlBase64ToUint8Array, pushManager.subscribe flowui/src/components/NotificationPermissionPrompt.tsx- Engagement-gated permission prompt with "Stay in the loop"ui/src/components/ChatPanel.tsx- Added agentResponseCount + rendered NotificationPermissionPrompt
Decisions Made
pushServiceuses named function exports (not a class), matching the existingchatServicepatterninitVapid()checks env vars before callingwebPush.setVapidDetails— no crash if unconfiguredsendPushToAllusesPromise.allSettledso one failed delivery doesn't block others- Stale 410/404 subscriptions are auto-deleted during send via
removeSubscription - DELETE /api/push/subscribe uses request body rather than URL param (endpoints are long URLs)
ui/src/api/push.tsuses a direct fetch for DELETE with body sinceapi.delete()has no body supportagentResponseCountderived viauseMemofiltering messages byrole === "assistant"urlBase64ToUint8Arrayreturns.buffer as ArrayBuffercast for TypeScript strict-modeapplicationServerKey
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Fixed TypeScript error in usePushNotifications.ts
- Found during: Task 2 — UI build
- Issue:
Uint8Array<ArrayBufferLike>is not assignable toapplicationServerKeytype in strict TypeScript - Fix: Changed
urlBase64ToUint8Array(publicKey)tourlBase64ToUint8Array(publicKey).buffer as ArrayBuffer - Files modified:
ui/src/hooks/usePushNotifications.ts - Verification: UI build passes after fix
- Committed in:
57d7a730(Task 2 commit)
2. [Rule 3 - Blocking] Installed missing idb dependency for UI
- Found during: Task 2 — UI build (pre-existing issue from plan 26-00 dependencies)
- Issue:
useOfflineQueue.tsimportsidbbut package was not installed inui/package.json - Fix:
pnpm --filter @paperclipai/ui add idb - Files modified:
ui/package.json,pnpm-lock.yaml - Verification: UI build passes after installing idb
- Committed in:
57d7a730(Task 2 commit)
Total deviations: 2 auto-fixed (1 bug, 1 blocking dependency) Impact on plan: Both fixes necessary for the build to succeed. No scope creep.
Issues Encountered
- Pre-existing TypeScript errors in
server/src/services/plugin-host-services.tsand missing@paperclipai/plugin-sdkmodule caused server build to fail, but these are unrelated to push notifications — verified no TS errors in new files specifically
User Setup Required
Push notifications require VAPID keys configured as environment variables:
VAPID_PUBLIC_KEY— VAPID public key (base64url)VAPID_PRIVATE_KEY— VAPID private key (base64url)VAPID_SUBJECT— Contact email, e.g.mailto:admin@nexus.local(defaults to this if unset)
Generate with: npx web-push generate-vapid-keys
Server initializes gracefully without these keys (push features disabled, no crash).
Next Phase Readiness
- Push notification infrastructure complete end-to-end
- Server can send push notifications to all subscriptions via
sendPushToAll(db, companyId, payload) - Client subscribes through SW pushManager after user grants permission
- Permission prompt appears after 3rd agent response, respects user dismiss state
- Phase 26 plan 05 (if any) can trigger push notifications on agent events using pushService
Phase: 26-pwa-performance Completed: 2026-04-02