d3ro-voice/docs/deployment/push-transport-without-firebase.md
Yun Chan c3ddd36c6f
Some checks failed
deploy-site / deploy (push) Failing after 40s
docs: record the 1.1.0 release and add the infrastructure map
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.
2026-09-16 23:27:52 +09:00

8.8 KiB
Raw Blame History

Push transport without Firebase — analysis

Status: decision note (2026-09-13) Scope: send-push Edge Function, server/supabase/migrations push outbox, apps/mobile-rn notification 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 (.p8 token) transports are implemented in send-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.tshttps://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 from send-push with a VAPID JWT (ES256).
  • No Firebase, no Google account. APNs not involved.

iOS — Firebase-free, implementable now

  • APNs directly: .p8 key + 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 .p8 as a secret and sign the JWT with WebCrypto.

Android — Firebase-free only via UnifiedPush

  • Google Play Services delivers notifications through FCM. The official FirebaseMessaging SDK mints the registration token, and it requires google-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.messaging scope, which means an FCM-enabled Google Cloud/Firebase project (the Firebase console is not strictly required, but the project is).
  • Firebase-free options:
    1. 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.
    2. 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.
    3. Third-party (OneSignal, Pusher Beams, Expo Push) — still FCM/APNs underneath. Does not remove Firebase for Android.

3. What "Firebase" actually costs us today

  • A Firebase project + google-services.json committed for the Android build (build blocks release if missing).
  • A service account JSON in Supabase secrets (FCM_SERVICE_ACCOUNT_JSON) and FCM_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.


  1. Keep Supabase as the SSOT/outbox (already true). Do not move token/outbox logic.
  2. [done] Implement webpush + apns transports in send-push (_shared/webpush.ts, _shared/apns.ts).
  3. 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.
  4. 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 SM 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 and provider: 'apns'.
  • Android decision: keep FCM or adopt UnifiedPush (see §2/§3).

  • server/supabase/functions/_shared/push-contract.ts — FCM transport + provider types
  • server/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 orchestration
  • server/cloudflare-worker/src/push-drain.ts — cron drain
  • apps/mobile-rn/src/features/notifications/* — client token/provider contract
  • docs/map/11-gap-backlog.md GAP-PUSH-01 / GAP-PUSH-02 / EXT-FIREBASE-01
  • docs/v3/MOBILE_APP_COMPLETION_SSOT.md EXT-011