---
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 `idb` dependency that was blocking UI build

## Task Commits

1. **Task 1: Backend — schema, migration, pushService, push routes** - `ad4cc035` (feat)
2. **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, deviceLabel
- `packages/db/src/migrations/0055_create_push_subscriptions.sql` - CREATE TABLE + endpoint index
- `packages/db/src/schema/index.ts` - Added export for pushSubscriptions
- `server/src/services/pushService.ts` - initVapid, getVapidPublicKey, saveSubscription, removeSubscription, sendPushToAll
- `server/src/routes/push.ts` - Express Router with GET /vapid-public-key, POST /subscribe, DELETE /subscribe
- `server/src/app.ts` - Import + mount pushRoutes at /api/push, call initVapid() at startup
- `ui/src/api/push.ts` - pushApi client with getVapidPublicKey, subscribe, unsubscribe methods
- `ui/src/hooks/usePushNotifications.ts` - Hook with urlBase64ToUint8Array, pushManager.subscribe flow
- `ui/src/components/NotificationPermissionPrompt.tsx` - Engagement-gated permission prompt with "Stay in the loop"
- `ui/src/components/ChatPanel.tsx` - Added agentResponseCount + rendered NotificationPermissionPrompt

## Decisions Made
- `pushService` uses named function exports (not a class), matching the existing `chatService` pattern
- `initVapid()` checks env vars before calling `webPush.setVapidDetails` — no crash if unconfigured
- `sendPushToAll` uses `Promise.allSettled` so 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.ts` uses a direct fetch for DELETE with body since `api.delete()` has no body support
- `agentResponseCount` derived via `useMemo` filtering messages by `role === "assistant"`
- `urlBase64ToUint8Array` returns `.buffer as ArrayBuffer` cast for TypeScript strict-mode `applicationServerKey`

## 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 to `applicationServerKey` type in strict TypeScript
- **Fix:** Changed `urlBase64ToUint8Array(publicKey)` to `urlBase64ToUint8Array(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.ts` imports `idb` but package was not installed in `ui/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.ts` and missing `@paperclipai/plugin-sdk` module 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*
