Supabase Push Notifications — Research
For Agents
Pure research note. No implementation has started. Captures viable transports, what plugs into the existing CRM stack, what needs to be built greenfield, iOS PWA gotchas, and open decisions to settle before planning. Topic surface: adding push to the mobile PWA (mobile-native-feel) — not the iOS native app (which is currently planning-only).
Headline Finding
Supabase has no native “push notifications” product. It provides the primitives — Postgres for tokens/subscriptions, Edge Functions for delivery, Database Webhooks for triggers — but the actual transport is bring-your-own.
That means picking a transport (Web Push / Expo Push / FCM-APNs direct), wiring the webhook → function delivery path ourselves, and handling subscription lifecycle (registration, expiration, cleanup).
Transport Options
| Option | Fit for CRM | Notes |
|---|---|---|
| Web Push (VAPID + Service Worker) | Strong — fits the existing PWA | Recommended starting point. Browser-native, no third-party SDK, works in iOS Safari 16.4+ standalone PWAs |
| Expo Push | Only useful once iOS native app exists | Unified iOS/Android abstraction — currently iOS app is planning-only, so deferred |
| FCM / APNs direct | More work, only if Web Push proves insufficient | Higher complexity, dual platform credentials, deeper SDK integration |
Phase A scope
Web Push for the mobile PWA is the shortest path to value. Everything below assumes Phase A unless noted.
What Already Exists in the Codebase
These components plug into a push delivery pipeline:
| Component | Location | Role in push pipeline |
|---|---|---|
vite-plugin-pwa (Workbox) | web/vite.config.ts | Already enabled. Would need to switch from generateSW → injectManifest strategy to add custom push + notificationclick handlers |
notify edge function | web/supabase/functions/notify/index.ts | Webhook-driven, already writes to notification table. Push delivery either extends this or becomes a sibling send-push function triggered by the same/sibling webhook |
notification table | Supabase | Already exists, RLS in place |
notification_preference table | Supabase | Already exists, RLS in place |
NotificationBell | web/src/components/... | In-app UI, complementary not replacement |
NotificationPopover | web/src/components/... | In-app UI |
NotificationItem | web/src/components/... | In-app UI |
useNotifications hook | web/src/lib/hooks/ | Existing data hook for in-app feed |
| iOS PWA meta tags | web/index.html | apple-mobile-web-app-capable + display: standalone already set — required for iOS push |
What Needs to Be Built (Greenfield)
1. Generate VAPID keys (one-time)
→ Public key in client bundle
→ Private key in Supabase secret (never in repo)
2. New table: push_subscriptions
├─ user_id (FK → person/user)
├─ subscription (jsonb — endpoint, keys.p256dh, keys.auth)
├─ created_at (timestamptz)
└─ last_seen_at (timestamptz)
With RLS: only the owning user can read/write their own rows
3. Service worker handlers
├─ self.addEventListener('push', ...)
└─ self.addEventListener('notificationclick', ...)
In `web/public/sw.js` (or wherever injectManifest lands the SW source)
4. Client hook: usePushNotifications
├─ Reads existing subscription state
├─ subscribe() — MUST be called from explicit user gesture (button click)
├─ unsubscribe()
└─ Persists to push_subscriptions via supabase upsert
5. Edge Function: send-push (or extension of `notify`)
├─ Imports `npm:web-push` library
├─ Loads VAPID private key from secret
├─ Fetches user's push_subscriptions rows
├─ Promise.allSettled() over endpoints
└─ DELETE on 410 Gone — essential for cleanup of expired subscriptions
6. DB Webhook
On INSERT into `notification` → fires send-push function
(Already a webhook on notification → notify; either extend it or add sibling)
7. Mobile detection hook (currently MISSING in codebase per research)
→ Conditionally surface the subscribe prompt only on mobile
→ Nothing in `web/src/mobile/lib/` matches today; closest is `useIsMobile`
but it's matchMedia-based — confirm that's sufficient for the prompt gate
iOS PWA Gotchas
iOS Web Push only works under specific conditions
- iOS 16.4+ — older iOS Safari has no Web Push at all
- Only after Add-to-Home-Screen (standalone display mode). Doesn’t work in regular Safari tab
apple-mobile-web-app-capablemust be set (already done inweb/index.html)display: standalonein manifest (already done)
CF Access ZTNA + iOS Standalone Cookie Spike — UNRESOLVED
The known cookie issue flagged in Locked Decisions (and pre-mortem) directly threatens push subscription. Symptom:
pushManager.subscribe()fires successfully, but the subsequent Supabase upsert topush_subscriptionsmay fail silently if cookies misbehave under iOS standalone PWA.This must be validated before committing to a plan. If the cookie issue is real, push subscriptions will appear to register from the user’s perspective but never actually persist server-side, leading to delivery failures with no error surface.
Other Gotchas
| Gotcha | Mitigation |
|---|---|
| DB webhook 1000ms timeout | Push fan-out must be fast, OR enqueue and let a background job deliver. For a single user with 1-2 devices, direct fan-out is fine; for broadcast pushes, queue |
| Subscription rot | Endpoints expire and return 410 Gone — must delete on 410 in the edge function, otherwise push_subscriptions will balloon with dead rows and waste delivery attempts |
| Service worker caching cross-user leak | SW must NOT cache user-scoped API responses. Browser caches are per-origin, not per-user, so a logged-in/logged-out switch could leak. Restrict caching to static assets + uses {credentials: 'include'} semantics carefully |
userspace/ has no PWA setup | Push doesn’t apply to the client portal until vite-plugin-pwa is added there. Out of scope for Phase A |
| Subscribe must be user-gesture-gated | pushManager.subscribe() requires user activation. Calling it on page load triggers permission denial in many browsers. Hook design must enforce gesture origin (button click only) |
Open Decisions Before Planning
- Scope — Web PWA only (Phase A) or also iOS native APNs (Phase B)? Recommendation: A first.
- Notification types — Which subset of existing notifications should push? Extend
notification_preferencewith apush_enabledboolean per type? Or a global setting? - Gating — Mobile-only or desktop too? Current PWA serves both; push permission UX differs (mobile users in-app vs desktop browser tab).
- Validate CF Access ZTNA + iOS standalone cookie spike — must happen first or the rest is wasted work.
Architecture Sketch (Phase A)
Web Push delivery pipeline
graph LR Trigger["Trigger event<br/>(e.g. task assigned)"] --> InsertNotif["INSERT into<br/>notification table"] InsertNotif --> Webhook["DB Webhook<br/>(1000ms budget)"] Webhook --> EdgeFn["Edge Function<br/>send-push"] EdgeFn --> LoadSubs["SELECT from<br/>push_subscriptions"] LoadSubs --> WebPush["web-push library<br/>VAPID-signed POST"] WebPush --> Browser["Push Service<br/>(FCM/Mozilla/Apple)"] Browser --> SW["Service Worker<br/>'push' handler"] SW --> Notif["showNotification()"] EdgeFn -.410 Gone.-> Cleanup["DELETE expired<br/>subscription"] style InsertNotif fill:#2d2d2d,stroke:#888,color:#fff style EdgeFn fill:#264653,stroke:#2a9d8f,color:#fff style WebPush fill:#264653,stroke:#2a9d8f,color:#fff style SW fill:#3d2020,stroke:#a44,color:#fff
Sources
- Supabase docs — Push notifications example
- Original Objective — From Lovable App to Mobile PWA push notifications with Supabase
- GitHub discussion — Supabase #13930
- andreinwald/webpush-ios-example — iOS-specific demo
Status
| Field | Value |
|---|---|
| Phase | Research only — no implementation started |
| Date | 2026-05-09 |
| Blocker | Validate CF Access ZTNA + iOS standalone cookie issue before planning |
| Next step | Settle the four open decisions above; then write a spec |
Related
- scheduled-reminders — Built on the centralized notification system this note researched; documents
CUSTOM_REMINDER, thesend-pushEFdefault_pushfallback fix, and why reminders bypassemit_notification(theevent_fanoutsilence) - mobile-notifications-ui — In-app notification surfaces (bell + drawer) that the snooze affordance hooks into
- mobile-native-feel — As-built mobile PWA (would be the surface receiving push)
- mobile-day-rituals — Day rituals that could trigger push (morning prompts, evening wrap-up)
- levandor-crm — Project overview / architecture reference
- integrations — External integrations (push transport sits alongside these)
- security — CF Access ZTNA auth — relates to the cookie blocker
- tech-debt — May surface the Mobile-detection-hook gap as a new entry