Some checks failed
deploy-site / deploy (push) Failing after 40s
Release notes for 1.1.0 were split between an Unreleased section and the version section, so the published notes would have omitted the update-feed and desktop changes. Everything shipping in this version now sits under one `## [1.1.0]` heading. `docs/map/` becomes the entry point for what infrastructure exists per platform and how far each feature is developed, with a documented update protocol so feature work and this map do not drift apart again. The release guide now states that installer binaries live in the update feed rather than the repository.
8.8 KiB
8.8 KiB
Push transport without Firebase — analysis
Status: decision note (2026-09-13) Scope:
send-pushEdge Function,server/supabase/migrationspush outbox,apps/mobile-rnnotification stack Question: we already pay for Supabase. Can it replace Firebase for notifications?
0. TL;DR
- Supabase already is the push backend. Tokens, devices, durable outbox, leases, retries, and delivery audit all live in Supabase (
push_tokens,devices,push_deliveries,push_dispatch_attempts,send-push). Firebase is only the last hop to the device. - Web and iOS are now Firebase-free on the server side. Web Push (VAPID + RFC 8291 aes128gcm) and Apple APNs (
.p8token) transports are implemented insend-push(_shared/webpush.ts,_shared/apns.ts, 10 unit tests). Client registration for those providers is the remaining work. - Android cannot drop Firebase without changing transport. Google Play Services only delivers push through FCM, and the FCM registration token is minted by the Firebase Messaging SDK, which requires
google-services.json(a Firebase project). Supabase cannot substitute for that hop. - The scheduled drain was missing and is now added. Nothing in the repo triggered
send-push?mode=drain, so transactionally enqueued notifications never left the outbox. A Cloudflare Cron Trigger now drains it every minute. - Firebase-free Android is possible but is a different product decision: UnifiedPush + a self-hosted distributor (e.g. ntfy). Third-party push services (OneSignal, Pusher) do not remove the Firebase dependency; they wrap FCM.
1. What exists today
| Layer | Owned by | Notes |
|---|---|---|
| Token + device registry | Supabase | push_tokens, devices; writes via register_push_registration RPC; RLS denies direct access |
| Durable outbox + retry/lease | Supabase | push_deliveries, push_dispatch_attempts, claim_due_push_dispatches, finalize_push_delivery |
| Dispatch orchestration | Supabase Edge send-push |
Auth, event resolution, batching, stale-token handling |
| Transport | FCM | _shared/push-contract.ts → https://fcm.googleapis.com/v1/projects/<id>/messages:send, OAuth2 via FCM_SERVICE_ACCOUNT_JSON |
| Android client | Firebase Messaging SDK | com.google.firebase:firebase-messaging, D3ROFirebaseMessagingService, google-services.json; FIREBASE_CONFIGURED build flag |
Provider values accepted but unimplemented (send-push/index.ts):
const unsupported = deliveries.filter((delivery) => delivery.provider !== 'fcm')
→ finalizeDelivery(..., 'permanent_failure', 'push_provider_not_supported')
So: the only Firebase-specific pieces are (a) the FCM HTTP v1 send + service account, and (b) the Android client token source.
2. Platform-by-platform answer
Desktop — no change needed
In-app events use IPC/EventEmitter and Supabase Realtime. No push transport, no Firebase.
Web — Firebase-free, implementable now
- Standard Web Push with VAPID keys. The browser Push API + service worker handles delivery; the server signs with a VAPID private key and POSTs to the subscription endpoint.
- Supabase plan: store
push_subscriptions(endpoint + keys per browser), send fromsend-pushwith a VAPID JWT (ES256). - No Firebase, no Google account. APNs not involved.
iOS — Firebase-free, implementable now
- APNs directly:
.p8key + Key ID + Team ID → JWT (ES256) →https://api.push.apple.com/3/device/<token>. - Needs an Apple Developer membership (already needed for App Store), not a Firebase project.
- Supabase Edge Function can hold the
.p8as a secret and sign the JWT with WebCrypto.
Android — Firebase-free only via UnifiedPush
- Google Play Services delivers notifications through FCM. The official
FirebaseMessagingSDK mints the registration token, and it requiresgoogle-services.json. There is no supported way to obtain an FCM token without a Firebase project. - FCM HTTP v1 also requires an OAuth2 service account with the
firebase.messagingscope, which means an FCM-enabled Google Cloud/Firebase project (the Firebase console is not strictly required, but the project is). - Firebase-free options:
- UnifiedPush + self-hosted distributor (ntfy) — open standard; the app registers with a distributor (
ntfy, NextPush) which holds a persistent connection; the server POSTs to the distributor. No Google. Requires the user to have a distributor installed (or the app to bundle one) and adds a background-connection battery cost. - Own persistent WebSocket / foreground service — free of Google but unreliable: Android Doze/App Standby kills sockets, so notifications cannot wake a killed app. Acceptable only for in-app live updates while running.
- Third-party (OneSignal, Pusher Beams, Expo Push) — still FCM/APNs underneath. Does not remove Firebase for Android.
- UnifiedPush + self-hosted distributor (ntfy) — open standard; the app registers with a distributor (
3. What "Firebase" actually costs us today
- A Firebase project +
google-services.jsoncommitted for the Android build (build blocks release if missing). - A service account JSON in Supabase secrets (
FCM_SERVICE_ACCOUNT_JSON) andFCM_PROJECT_ID. - No Firebase database/auth/storage/analytics is used. It is a push-only dependency.
If the goal is "no Firebase at all", the decision reduces to: replace Android transport (UnifiedPush) or accept FCM on Android only.
4. Recommended path
- Keep Supabase as the SSOT/outbox (already true). Do not move token/outbox logic.
- [done] Implement
webpush+apnstransports insend-push(_shared/webpush.ts,_shared/apns.ts). - For Android, offer two modes behind config:
fcm(default): minimal Firebase project, push-only usage.unifiedpush(opt-in): self-hosted ntfy distributor; documented UX and battery caveats.
- Do not adopt OneSignal/Pusher as a "Firebase replacement" — it does not remove the dependency.
Effort sketch
| Item | Effort | Risk |
|---|---|---|
| Web Push (VAPID) transport + tests | S–M | low — done |
APNs (JWT ES256 .p8) transport + tests |
M | medium — server done, Apple key + client registration pending |
| Cloudflare cron drain | S | low — done |
| UnifiedPush Android (client lib + distributor + server transport) | L | high (UX, battery, distributor dependency) |
| Minimal Firebase project for Android FCM | S (external action) | low |
5. Implemented (2026-09-13)
| Piece | Where | Notes |
|---|---|---|
| Web Push transport | _shared/webpush.ts |
VAPID ES256 JWT + RFC 8291 aes128gcm encryption; strict config/subscription validation; 200/404/410/413 mapping |
| APNs transport | _shared/apns.ts |
Token-based .p8 ES256 JWT (cached ~50 min); production/sandbox hosts; stale (BadDeviceToken/Unregistered) and 403 handling |
| Provider routing | send-push/index.ts |
fcm / webpush / apns dispatched with per-delivery stale → stale, payload → permanent_failure, else retryable; unknown providers stay push_provider_not_supported |
| Cloudflare cron drain | server/cloudflare-worker/src/push-drain.ts + scheduled() + wrangler.toml [triggers] crons |
POSTs send-push?mode=drain&limit=… with the service-role bearer every minute |
| Tests | _shared/webpush.test.ts, _shared/apns.test.ts, push-drain.test.ts, push-contract.test.ts |
10 new tests incl. an RFC 8291 decrypt round-trip; CI runs the edge suite and the worker drain test |
Required configuration
| Secret / var | Consumer | Purpose |
|---|---|---|
WEBPUSH_VAPID_PUBLIC_KEY / WEBPUSH_VAPID_PRIVATE_KEY / WEBPUSH_SUBJECT |
Supabase Edge Function | VAPID signing; subject must be mailto: or https: |
APNS_KEY_ID / APNS_TEAM_ID / APNS_PRIVATE_KEY / APNS_TOPIC / APNS_ENVIRONMENT |
Supabase Edge Function | APNs token auth; topic is the bundle id (com.d3ro.voice) |
SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / PUSH_DRAIN_BATCH_LIMIT |
Cloudflare Worker | Cron drain target + auth (service role key as wrangler secret) |
Remaining work
- Client registration: the mobile app currently registers only
fcm. Web needs a service worker +pushManager.subscribe→ store the JSON subscription as the registration id; iOS needs an APNs device token andprovider: 'apns'. - Android decision: keep FCM or adopt UnifiedPush (see §2/§3).
6. Related
server/supabase/functions/_shared/push-contract.ts— FCM transport + provider typesserver/supabase/functions/_shared/webpush.ts— Web Push (VAPID + RFC 8291)server/supabase/functions/_shared/apns.ts— APNs (.p8 token)server/supabase/functions/send-push/index.ts— dispatch orchestrationserver/cloudflare-worker/src/push-drain.ts— cron drainapps/mobile-rn/src/features/notifications/*— client token/provider contractdocs/map/11-gap-backlog.mdGAP-PUSH-01/GAP-PUSH-02/EXT-FIREBASE-01docs/v3/MOBILE_APP_COMPLETION_SSOT.mdEXT-011