MarketplaceSellers
Docs
    GreenFood.live

    Decentralized Self-building Future

    Sign In
    Marketplace
    Sellers
    Group Buy
    Privacy|Contact
    v1.104.12|Sonoratek LLC

    Documentation

    Concepts, value, and typical clone scenarios — less code.

    Quick Reference

    Quick entry (CTOs · auditors · agents)

    Welcome — mission & audiences
    Quick Reference
    Getting started
    Architecture & Auth.js
    Backend modes & databases (DB_BACKEND_MODE)
    Self-hosted
    Ring MCP Tools
    Ring MCP Server
    Token economics
    Token launch jurisdictions
    Deploy (Docker · k8s)
    Security & compliance reads
    GitHub — connectplatform/ring
    Source — MIT license (GitHub)

    Documentation

    Concepts, value, and typical clone scenarios — less code.

    Quick Reference

    Quick entry (CTOs · auditors · agents)

    Welcome — mission & audiences
    Quick Reference
    Getting started
    Architecture & Auth.js
    Backend modes & databases (DB_BACKEND_MODE)
    Self-hosted
    Ring MCP Tools
    Ring MCP Server
    Token economics
    Token launch jurisdictions
    Deploy (Docker · k8s)
    Security & compliance reads
    GitHub — connectplatform/ring
    Source — MIT license (GitHub)

    Documentation

    Concepts, value, and typical clone scenarios — less code.

    Quick Reference

    Quick entry (CTOs · auditors · agents)

    Welcome — mission & audiences
    Quick Reference
    Getting started
    Architecture & Auth.js
    Backend modes & databases (DB_BACKEND_MODE)
    Self-hosted
    Ring MCP Tools
    Ring MCP Server
    Token economics
    Token launch jurisdictions
    Deploy (Docker · k8s)
    Security & compliance reads
    GitHub — connectplatform/ring
    Source — MIT license (GitHub)
    GreenFood.live Logo

    Loading documentation...

    Preparing GreenFood.live content

    GreenFood.live Logo

    Loading documentation...

    Preparing GreenFood.live content

    GreenFood.live Logo

    Loading documentation...

    Preparing GreenFood.live content

    Push Notifications with FCM (Ring-Powered)

    Use the Founder / Developer tabs to filter this page. FCM works in every DB_BACKEND_MODE (k8s-postgres-fcm, supabase-fcm, firebase-full) — you do not need Firestore for push alone.

    Ring stores FCM device tokens in your primary database (PostgreSQL or Firestore by backend mode), one row per user per device. Browser clients obtain a token with the Firebase Messaging SDK; the server sends with the Firebase Admin SDK. RFC Web Push (web-push + push_subscriptions) dual-dispatches on NotificationChannel.PUSH when VAPID_* are set — used when FCM cannot own the PushManager subscription (Safari / no prior FCM subscribe).

    Firebase Web Push certificate vs dedicated VAPID_*

    FCM (getToken): NEXT_PUBLIC_FIREBASE_VAPID_KEY is the public key from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates for the same Firebase project as NEXT_PUBLIC_FIREBASE_PROJECT_ID. Only this value is passed to getToken({ vapidKey }) via getFcmVapidKey().

    RFC Web Push: VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT are runtime secrets (not Docker build-args). Generate with npx web-push generate-vapid-keys. Never pass VAPID_* into getToken. One PushManager subscription per service-worker scope: FCM browsers stay on fcm_tokens; RFC subscribe only when no subscription exists yet. Code: features/notifications/services/webpush-service.ts, GET /api/push/vapid-public, hooks/use-fcm.ts registerRfcWebPushSubscription(), migration 046_push_subscriptions_jsonb.sql.

    Notifications overview

    Notifications API

    Backend modes

    Firebase integration

    Why FCM matters for your clone

    • Reach users when the tab is closed — FCM delivers OS/browser push; Tunnel covers live in-app inbox while they are online
    • One row per device — phone + laptop + tablet stay separate; invalid tokens are cleaned when FCM reports them dead
    • Works without full Firestore — Postgres-primary clones still use Firebase only for Cloud Messaging. Set integrations.firebase to { enabled: false, fcmEnabled: true, firestoreEnabled: false }; the client initializes the Firebase app for messaging only.
    • Per-clone Firebase project — each white-label needs its own Console Web Push certificate; copying another project’s public key breaks getToken

    Typical scenarios

    ScenarioWhat to do
    New clone / white-labelCreate (or reuse) a Firebase project for that clone; generate Web Push certificates there; set NEXT_PUBLIC_FIREBASE_* + NEXT_PUBLIC_FIREBASE_VAPID_KEY
    Push “not working” after copy-paste envCertificate must belong to this clone’s NEXT_PUBLIC_FIREBASE_PROJECT_ID. Firebase getToken fails on mismatch — regenerate in Console, then rebuild the image (browser bundle is build-time)

    Shipped stack (FCM + RFC dual-stack)

    LayerFCM (primary web)RFC Web Push
    Client subscribesubscribeWithVapid() in hooks/use-fcm.ts → getToken({ vapidKey: getFcmVapidKey() })PushManager.subscribe with VAPID_PUBLIC_KEY only if no existing subscription
    Service workerpublic/firebase-messaging-sw.jspublic/push-sw.js when FCM is not configured; never both on scope /
    Client enableisFcmConfigured() (Firebase public env)GET /api/push/vapid-public — prompt does not require Firebase
    RegisterPOST /api/notifications/fcm/register (preferred; Server Action upsertFcmToken still exists)Server Action upsertPushSubscription; public key via GET /api/push/vapid-public

    Frequently asked questions

    Impact

    Will Chrome users get two OS banners for one chat message?

    No. Chrome FCM occupies PushManager, so RFC subscribe is skipped and push_subscriptions stays empty (web-push attempted: 0). Same account on Safari is a second device, not a second Chrome pipe.

    Do iPhone users get FCM?

    Not in a Safari tab. They need the Home Screen web app (iOS 16.4+), then RFC PushManager.subscribe with dedicated VAPID_*. Chrome on iOS is still WebKit — same Home Screen rule.

    Migration

    Can I reuse the Firebase Console certificate as VAPID_PUBLIC_KEY?

    No. That public key has no usable RFC private. Generate npx web-push generate-vapid-keys and store the trio as runtime Secret.

    Ops

    Why does validate-fcm-env --format yaml show a 2-character Admin key?

    AUTH_FIREBASE_PRIVATE_KEY: |- is a YAML block scalar. Use ring/scripts/validate-fcm-env.mjs (block-scalar parser), not a one-line regex.

    Related documentation

    Related documentation

    Notifications

    Prerequisite: notification types and channels before deep FCM setup.

    Notifications

    Same-workflow: FCM register API contract and env truth for push.

    Firebase

    Deep-dive: Firebase Admin, client SDK, and FCM adapter bridge.

    K8s Postgres Fcm

    Depends-on: Postgres-primary mode where FCM tokens live in SQL.

    Backend Modes And Databases

    Push Notifications with FCM (Ring-Powered)

    Use the Founder / Developer tabs to filter this page. FCM works in every DB_BACKEND_MODE (k8s-postgres-fcm, supabase-fcm, firebase-full) — you do not need Firestore for push alone.

    Ring stores FCM device tokens in your primary database (PostgreSQL or Firestore by backend mode), one row per user per device. Browser clients obtain a token with the Firebase Messaging SDK; the server sends with the Firebase Admin SDK. RFC Web Push (web-push + push_subscriptions) dual-dispatches on NotificationChannel.PUSH when VAPID_* are set — used when FCM cannot own the PushManager subscription (Safari / no prior FCM subscribe).

    Firebase Web Push certificate vs dedicated VAPID_*

    FCM (getToken): NEXT_PUBLIC_FIREBASE_VAPID_KEY is the public key from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates for the same Firebase project as NEXT_PUBLIC_FIREBASE_PROJECT_ID. Only this value is passed to getToken({ vapidKey }) via getFcmVapidKey().

    RFC Web Push: VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT are runtime secrets (not Docker build-args). Generate with npx web-push generate-vapid-keys. Never pass VAPID_* into getToken. One PushManager subscription per service-worker scope: FCM browsers stay on fcm_tokens; RFC subscribe only when no subscription exists yet. Code: features/notifications/services/webpush-service.ts, GET /api/push/vapid-public, hooks/use-fcm.ts registerRfcWebPushSubscription(), migration 046_push_subscriptions_jsonb.sql.

    Notifications overview

    Notifications API

    Backend modes

    Firebase integration

    Why FCM matters for your clone

    • Reach users when the tab is closed — FCM delivers OS/browser push; Tunnel covers live in-app inbox while they are online
    • One row per device — phone + laptop + tablet stay separate; invalid tokens are cleaned when FCM reports them dead
    • Works without full Firestore — Postgres-primary clones still use Firebase only for Cloud Messaging. Set integrations.firebase to { enabled: false, fcmEnabled: true, firestoreEnabled: false }; the client initializes the Firebase app for messaging only.
    • Per-clone Firebase project — each white-label needs its own Console Web Push certificate; copying another project’s public key breaks getToken

    Typical scenarios

    ScenarioWhat to do
    New clone / white-labelCreate (or reuse) a Firebase project for that clone; generate Web Push certificates there; set NEXT_PUBLIC_FIREBASE_* + NEXT_PUBLIC_FIREBASE_VAPID_KEY
    Push “not working” after copy-paste envCertificate must belong to this clone’s NEXT_PUBLIC_FIREBASE_PROJECT_ID. Firebase getToken fails on mismatch — regenerate in Console, then rebuild the image (browser bundle is build-time)

    Shipped stack (FCM + RFC dual-stack)

    LayerFCM (primary web)RFC Web Push
    Client subscribesubscribeWithVapid() in hooks/use-fcm.ts → getToken({ vapidKey: getFcmVapidKey() })PushManager.subscribe with VAPID_PUBLIC_KEY only if no existing subscription
    Service workerpublic/firebase-messaging-sw.jspublic/push-sw.js when FCM is not configured; never both on scope /
    Client enableisFcmConfigured() (Firebase public env)GET /api/push/vapid-public — prompt does not require Firebase
    RegisterPOST /api/notifications/fcm/register (preferred; Server Action upsertFcmToken still exists)Server Action upsertPushSubscription; public key via GET /api/push/vapid-public

    Frequently asked questions

    Impact

    Will Chrome users get two OS banners for one chat message?

    No. Chrome FCM occupies PushManager, so RFC subscribe is skipped and push_subscriptions stays empty (web-push attempted: 0). Same account on Safari is a second device, not a second Chrome pipe.

    Do iPhone users get FCM?

    Not in a Safari tab. They need the Home Screen web app (iOS 16.4+), then RFC PushManager.subscribe with dedicated VAPID_*. Chrome on iOS is still WebKit — same Home Screen rule.

    Migration

    Can I reuse the Firebase Console certificate as VAPID_PUBLIC_KEY?

    No. That public key has no usable RFC private. Generate npx web-push generate-vapid-keys and store the trio as runtime Secret.

    Ops

    Why does validate-fcm-env --format yaml show a 2-character Admin key?

    AUTH_FIREBASE_PRIVATE_KEY: |- is a YAML block scalar. Use ring/scripts/validate-fcm-env.mjs (block-scalar parser), not a one-line regex.

    Related documentation

    Related documentation

    Notifications

    Prerequisite: notification types and channels before deep FCM setup.

    Notifications

    Same-workflow: FCM register API contract and env truth for push.

    Firebase

    Deep-dive: Firebase Admin, client SDK, and FCM adapter bridge.

    K8s Postgres Fcm

    Depends-on: Postgres-primary mode where FCM tokens live in SQL.

    Backend Modes And Databases

    Push Notifications with FCM (Ring-Powered)

    Use the Founder / Developer tabs to filter this page. FCM works in every DB_BACKEND_MODE (k8s-postgres-fcm, supabase-fcm, firebase-full) — you do not need Firestore for push alone.

    Ring stores FCM device tokens in your primary database (PostgreSQL or Firestore by backend mode), one row per user per device. Browser clients obtain a token with the Firebase Messaging SDK; the server sends with the Firebase Admin SDK. RFC Web Push (web-push + push_subscriptions) dual-dispatches on NotificationChannel.PUSH when VAPID_* are set — used when FCM cannot own the PushManager subscription (Safari / no prior FCM subscribe).

    Firebase Web Push certificate vs dedicated VAPID_*

    FCM (getToken): NEXT_PUBLIC_FIREBASE_VAPID_KEY is the public key from Firebase Console → Project Settings → Cloud Messaging → Web Push certificates for the same Firebase project as NEXT_PUBLIC_FIREBASE_PROJECT_ID. Only this value is passed to getToken({ vapidKey }) via getFcmVapidKey().

    RFC Web Push: VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT are runtime secrets (not Docker build-args). Generate with npx web-push generate-vapid-keys. Never pass VAPID_* into getToken. One PushManager subscription per service-worker scope: FCM browsers stay on fcm_tokens; RFC subscribe only when no subscription exists yet. Code: features/notifications/services/webpush-service.ts, GET /api/push/vapid-public, hooks/use-fcm.ts registerRfcWebPushSubscription(), migration 046_push_subscriptions_jsonb.sql.

    Notifications overview

    Notifications API

    Backend modes

    Firebase integration

    Why FCM matters for your clone

    • Reach users when the tab is closed — FCM delivers OS/browser push; Tunnel covers live in-app inbox while they are online
    • One row per device — phone + laptop + tablet stay separate; invalid tokens are cleaned when FCM reports them dead
    • Works without full Firestore — Postgres-primary clones still use Firebase only for Cloud Messaging. Set integrations.firebase to { enabled: false, fcmEnabled: true, firestoreEnabled: false }; the client initializes the Firebase app for messaging only.
    • Per-clone Firebase project — each white-label needs its own Console Web Push certificate; copying another project’s public key breaks getToken

    Typical scenarios

    ScenarioWhat to do
    New clone / white-labelCreate (or reuse) a Firebase project for that clone; generate Web Push certificates there; set NEXT_PUBLIC_FIREBASE_* + NEXT_PUBLIC_FIREBASE_VAPID_KEY
    Push “not working” after copy-paste envCertificate must belong to this clone’s NEXT_PUBLIC_FIREBASE_PROJECT_ID. Firebase getToken fails on mismatch — regenerate in Console, then rebuild the image (browser bundle is build-time)

    Shipped stack (FCM + RFC dual-stack)

    LayerFCM (primary web)RFC Web Push
    Client subscribesubscribeWithVapid() in hooks/use-fcm.ts → getToken({ vapidKey: getFcmVapidKey() })PushManager.subscribe with VAPID_PUBLIC_KEY only if no existing subscription
    Service workerpublic/firebase-messaging-sw.jspublic/push-sw.js when FCM is not configured; never both on scope /
    Client enableisFcmConfigured() (Firebase public env)GET /api/push/vapid-public — prompt does not require Firebase
    RegisterPOST /api/notifications/fcm/register (preferred; Server Action upsertFcmToken still exists)Server Action upsertPushSubscription; public key via GET /api/push/vapid-public

    Frequently asked questions

    Impact

    Will Chrome users get two OS banners for one chat message?

    No. Chrome FCM occupies PushManager, so RFC subscribe is skipped and push_subscriptions stays empty (web-push attempted: 0). Same account on Safari is a second device, not a second Chrome pipe.

    Do iPhone users get FCM?

    Not in a Safari tab. They need the Home Screen web app (iOS 16.4+), then RFC PushManager.subscribe with dedicated VAPID_*. Chrome on iOS is still WebKit — same Home Screen rule.

    Migration

    Can I reuse the Firebase Console certificate as VAPID_PUBLIC_KEY?

    No. That public key has no usable RFC private. Generate npx web-push generate-vapid-keys and store the trio as runtime Secret.

    Ops

    Why does validate-fcm-env --format yaml show a 2-character Admin key?

    AUTH_FIREBASE_PRIVATE_KEY: |- is a YAML block scalar. Use ring/scripts/validate-fcm-env.mjs (block-scalar parser), not a one-line regex.

    Related documentation

    Related documentation

    Notifications

    Prerequisite: notification types and channels before deep FCM setup.

    Notifications

    Same-workflow: FCM register API contract and env truth for push.

    Firebase

    Deep-dive: Firebase Admin, client SDK, and FCM adapter bridge.

    K8s Postgres Fcm

    Depends-on: Postgres-primary mode where FCM tokens live in SQL.

    Backend Modes And Databases

    RFC dual-stack (Chrome + Safari)
    Dedicated keypair (npx web-push generate-vapid-keys) in runtime Secret as VAPID_* — not the Console certificate
    RFC-only Safari (no Firebase)Set VAPID_* runtime secrets. iPhone/iPad: Add to Home Screen (display: standalone). Chrome still uses FCM when NEXT_PUBLIC_FIREBASE_* is present.

    Operator checklist (shipped)

    1. Firebase project with Cloud Messaging enabled
    2. Web Push certificate generated in that same project → NEXT_PUBLIC_FIREBASE_VAPID_KEY (Docker build-arg)
    3. Server service account → AUTH_FIREBASE_PROJECT_ID / AUTH_FIREBASE_CLIENT_EMAIL / AUTH_FIREBASE_PRIVATE_KEY
    4. HTTPS (or localhost) + service worker at /firebase-messaging-sw.js
    5. Signed-in users (Auth.js) before token registration
    6. After rotating the certificate: rebuild/redeploy — ConfigMap alone does not refresh the browser bundle
    7. RFC (Safari / Firebase-less): VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT as runtime Secret; iOS users must open the Home Screen web app
    8. iPhone/iPad: Share → Add to Home Screen → open the icon → Enable (Safari tabs have no PushManager). Need a real 180×180 /apple-touch-icon.png so iOS saves a web app, not a bookmark
    Store
    fcm_tokens via lib/notifications/fcm-token-db.ts
    push_subscriptions JSONB (migration 046)
    Server sendfcm-service.ts Admin HTTP v1 sendEachwebpush-service.ts (web-push); dual Promise.allSettled on PUSH
    npmfirebase, firebase-adminweb-push

    Env map

    VariableRoleStatus
    NEXT_PUBLIC_FIREBASE_VAPID_KEYConsole Web Push certificate (public) for getToken()Required for FCM — build-arg; read via getFcmVapidKey(). Not needed on RFC-only Safari
    VAPID_PUBLIC_KEYDedicated RFC public for web-push / PushManagerRuntime Secret — must differ from Console cert
    VAPID_PRIVATE_KEYDedicated RFC private (server-only)Runtime Secret — never a build-arg
    VAPID_SUBJECTmailto: or https: contact URIRuntime Secret

    FCM send uses Admin SDK (AUTH_FIREBASE_*), not VAPID_PRIVATE_KEY. isFcmConfigured() is presence + non-placeholder only (validateFirebaseConfig() && validateFcmVapidKey()). RFC-only browsers set isSupported from Push APIs + /api/push/vapid-public, not from Firebase. Mismatched FCM certificates surface as Firebase getToken errors — Ring does not classify key prefixes.

    SurfaceChrome / FCMSafari RFC (macOS tab or iOS Home Screen PWA)
    SubscribegetToken → fcm_tokensPushManager.subscribe only if no existing subscription → push_subscriptions
    Focused tabIn-app toast; CALL_INVITE / GAME_REQUEST → global banners + ringtone (no new Notification(), no navigate)Tunnel banners when the tab is open; SW showNotification (WebKit forbids silent push)
    Hidden / killedFCM SW OS banner (onBackgroundMessage; data-only for call/game)RFC SW OS banner (push-sw.js)
    Missed call TTL~90s (webpush.headers.TTL)~90s (web-push TTL)

    How server PUSH dual-dispatch works

    NotificationChannel.PUSH always calls both Firebase Admin (fcm_tokens) and RFC web-push (push_subscriptions). That is per-user device-class fan-out, not two deliveries to Chrome:

    DeviceTable rowWhat the other stack does
    Chrome (FCM getToken owns PushManager)fcm_tokensRFC subscribe skipped → no push_subscriptions row → web-push attempted: 0 (no-op)
    Safari RFC-onlypush_subscriptionsNo FCM token → Admin attempted: 0 (no-op)
    Same account, Chrome laptop + Safari phonebothboth sends are required

    Do not skip RFC globally when FCM is configured — that would drop the Safari device. Empty push_subscriptions on a Chrome-only user is expected, not a missing second pipe.

    Delivery accounting (lib/notifications/push-dual-dispatch.ts): delivered if either stack sent > 0; failed if endpoints existed but every send failed; noop (still recorded DELIVERED on the PUSH channel) when both tables are empty.

    Build-time vs runtime

    NEXT_PUBLIC_FIREBASE_VAPID_KEY is inlined into the Next.js client bundle at image build. Cluster ConfigMap can update process env, but browsers keep the previous inlined value until rebuild. VAPID_* are runtime-only — rotate without rebuilding the client bundle.

    Token flow

    FCM token register and send

    Prerequisites

    • Auth.js session — server derives user_id
    • HTTPS or localhost; SW at /firebase-messaging-sw.js (FCM) or /push-sw.js (RFC-only)
    • Valid NEXT_PUBLIC_FIREBASE_* + this project’s Console Web Push certificate or RFC-only VAPID_* (Safari / no Firebase)
    • Server Admin: AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY

    Register path

    1. 1

      Stable device fingerprint

      Persist a UUID (e.g. localStorage) and reuse it. Upsert key is (user_id, device_fingerprint).

    2. 2

      Permission + FCM token

      Notification.requestPermission(), then subscribeWithVapid() → Firebase getToken with getFcmVapidKey() (hooks/use-fcm.ts). FCMProviderRuntime owns one useFCM() instance; useFCMMessages(onMessageReceived, isSupported) must not call useFCM again.

    3. 3

      Persist on the server

      POST /api/notifications/fcm/register (avoids Server Action RSC revalidation). Body: token, deviceFingerprint, optional deviceInfo / platform. Auth required.

    4. 4

      Unregister on logout

      useAuth().signOut() runs device unregister. Non-React: DELETE /api/notifications/fcm/register with the same deviceFingerprint.

    Server send (verified modules)

    ModulePath
    Domain FCM send + invalid-token cleanupfeatures/notifications/services/fcm-service.ts
    firebase-full adapter bridgelib/database/adapters/FirebaseAdapter.ts — sendFcmMessage, sendFcmToUser, sendFcmToTopic, validateFcmToken, cleanupInvalidFcmTokens
    Admin messaging accessorlib/firebase-admin.server.ts → getAdminMessaging()
    Token DB layerlib/notifications/fcm-token-db.ts
    RFC sendfeatures/notifications/services/webpush-service.ts
    Client hook / SWhooks/use-fcm.ts, public/firebase-messaging-sw.js

    PostgreSQL: data/migrations/016_fcm_jsonb_schema.sql, 046_push_subscriptions_jsonb.sql.

    Environment

    PurposeVariablesWhere
    Firebase client + FCM subscribeNEXT_PUBLIC_FIREBASE_* + NEXT_PUBLIC_FIREBASE_VAPID_KEYClient / Docker build-arg
    Admin sendAUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY; DB_BACKEND_MODEServer-only
    RFC Web PushVAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECTRuntime Secret — webpush-service.ts

    Troubleshooting

    IssueCauseFix
    token-subscribe-failed / auth credentialCertificate does not belong to this Firebase project, or Cloud Messaging API disabledGenerate Web Push certificates in this project’s Console; rebuild image; enable FCM + Installations APIs
    ConfigMap updated but browser still oldNEXT_PUBLIC_* inlined at buildRebuild/redeploy with matching build-arg
    Permission deniedUser denied notificationsRequest permission before getToken()
    401 on registerNo sessionCookie / Bearer required
    UNREGISTERED on sendStale tokenFCMService / sendFcmToUser marks row invalid
    RFC web-push emptyMissing VAPID_* trio, Firebase-less Safari never prompted, or PushManager already owned by FCMSet dedicated runtime secrets; RFC isSupported does not need NEXT_PUBLIC_FIREBASE_*; iOS = Home Screen PWA. Chrome-only users should have empty push_subscriptions
    Foreground toast + OS banneruseFCMMessages used to call new Notification(), or FCM SW push + onBackgroundMessage both showingFocused tab is toast (or call/game banner) only; FCM SW skips push when messaging init succeeded; data-only for call/game
    validate-fcm-env --format yaml private key ~2 charsOld regex captured `-` as the value
    Call-invite dedupe differs across podsREDIS_URL unset → in-process MapOptional Redis; k3s-or ring-platform-org is 1 replica today
    Disable then push returnsNotification.permission stays granted; init effect used to re-subscribering_push_opt_out localStorage + unregisterCurrentDevicePush

    See-also: how token storage routes by DB_BACKEND_MODE.

    Webrtc Calls

    Same-workflow: CALL_INVITE offline push uses this dual-stack (data-only FCM, 90s TTL).

    Peer Games

    Same-workflow: GAME_REQUEST offline push after Tunnel presence grace.

    RFC dual-stack (Chrome + Safari)
    Dedicated keypair (npx web-push generate-vapid-keys) in runtime Secret as VAPID_* — not the Console certificate
    RFC-only Safari (no Firebase)Set VAPID_* runtime secrets. iPhone/iPad: Add to Home Screen (display: standalone). Chrome still uses FCM when NEXT_PUBLIC_FIREBASE_* is present.

    Operator checklist (shipped)

    1. Firebase project with Cloud Messaging enabled
    2. Web Push certificate generated in that same project → NEXT_PUBLIC_FIREBASE_VAPID_KEY (Docker build-arg)
    3. Server service account → AUTH_FIREBASE_PROJECT_ID / AUTH_FIREBASE_CLIENT_EMAIL / AUTH_FIREBASE_PRIVATE_KEY
    4. HTTPS (or localhost) + service worker at /firebase-messaging-sw.js
    5. Signed-in users (Auth.js) before token registration
    6. After rotating the certificate: rebuild/redeploy — ConfigMap alone does not refresh the browser bundle
    7. RFC (Safari / Firebase-less): VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT as runtime Secret; iOS users must open the Home Screen web app
    8. iPhone/iPad: Share → Add to Home Screen → open the icon → Enable (Safari tabs have no PushManager). Need a real 180×180 /apple-touch-icon.png so iOS saves a web app, not a bookmark
    Store
    fcm_tokens via lib/notifications/fcm-token-db.ts
    push_subscriptions JSONB (migration 046)
    Server sendfcm-service.ts Admin HTTP v1 sendEachwebpush-service.ts (web-push); dual Promise.allSettled on PUSH
    npmfirebase, firebase-adminweb-push

    Env map

    VariableRoleStatus
    NEXT_PUBLIC_FIREBASE_VAPID_KEYConsole Web Push certificate (public) for getToken()Required for FCM — build-arg; read via getFcmVapidKey(). Not needed on RFC-only Safari
    VAPID_PUBLIC_KEYDedicated RFC public for web-push / PushManagerRuntime Secret — must differ from Console cert
    VAPID_PRIVATE_KEYDedicated RFC private (server-only)Runtime Secret — never a build-arg
    VAPID_SUBJECTmailto: or https: contact URIRuntime Secret

    FCM send uses Admin SDK (AUTH_FIREBASE_*), not VAPID_PRIVATE_KEY. isFcmConfigured() is presence + non-placeholder only (validateFirebaseConfig() && validateFcmVapidKey()). RFC-only browsers set isSupported from Push APIs + /api/push/vapid-public, not from Firebase. Mismatched FCM certificates surface as Firebase getToken errors — Ring does not classify key prefixes.

    SurfaceChrome / FCMSafari RFC (macOS tab or iOS Home Screen PWA)
    SubscribegetToken → fcm_tokensPushManager.subscribe only if no existing subscription → push_subscriptions
    Focused tabIn-app toast; CALL_INVITE / GAME_REQUEST → global banners + ringtone (no new Notification(), no navigate)Tunnel banners when the tab is open; SW showNotification (WebKit forbids silent push)
    Hidden / killedFCM SW OS banner (onBackgroundMessage; data-only for call/game)RFC SW OS banner (push-sw.js)
    Missed call TTL~90s (webpush.headers.TTL)~90s (web-push TTL)

    How server PUSH dual-dispatch works

    NotificationChannel.PUSH always calls both Firebase Admin (fcm_tokens) and RFC web-push (push_subscriptions). That is per-user device-class fan-out, not two deliveries to Chrome:

    DeviceTable rowWhat the other stack does
    Chrome (FCM getToken owns PushManager)fcm_tokensRFC subscribe skipped → no push_subscriptions row → web-push attempted: 0 (no-op)
    Safari RFC-onlypush_subscriptionsNo FCM token → Admin attempted: 0 (no-op)
    Same account, Chrome laptop + Safari phonebothboth sends are required

    Do not skip RFC globally when FCM is configured — that would drop the Safari device. Empty push_subscriptions on a Chrome-only user is expected, not a missing second pipe.

    Delivery accounting (lib/notifications/push-dual-dispatch.ts): delivered if either stack sent > 0; failed if endpoints existed but every send failed; noop (still recorded DELIVERED on the PUSH channel) when both tables are empty.

    Build-time vs runtime

    NEXT_PUBLIC_FIREBASE_VAPID_KEY is inlined into the Next.js client bundle at image build. Cluster ConfigMap can update process env, but browsers keep the previous inlined value until rebuild. VAPID_* are runtime-only — rotate without rebuilding the client bundle.

    Token flow

    FCM token register and send

    Prerequisites

    • Auth.js session — server derives user_id
    • HTTPS or localhost; SW at /firebase-messaging-sw.js (FCM) or /push-sw.js (RFC-only)
    • Valid NEXT_PUBLIC_FIREBASE_* + this project’s Console Web Push certificate or RFC-only VAPID_* (Safari / no Firebase)
    • Server Admin: AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY

    Register path

    1. 1

      Stable device fingerprint

      Persist a UUID (e.g. localStorage) and reuse it. Upsert key is (user_id, device_fingerprint).

    2. 2

      Permission + FCM token

      Notification.requestPermission(), then subscribeWithVapid() → Firebase getToken with getFcmVapidKey() (hooks/use-fcm.ts). FCMProviderRuntime owns one useFCM() instance; useFCMMessages(onMessageReceived, isSupported) must not call useFCM again.

    3. 3

      Persist on the server

      POST /api/notifications/fcm/register (avoids Server Action RSC revalidation). Body: token, deviceFingerprint, optional deviceInfo / platform. Auth required.

    4. 4

      Unregister on logout

      useAuth().signOut() runs device unregister. Non-React: DELETE /api/notifications/fcm/register with the same deviceFingerprint.

    Server send (verified modules)

    ModulePath
    Domain FCM send + invalid-token cleanupfeatures/notifications/services/fcm-service.ts
    firebase-full adapter bridgelib/database/adapters/FirebaseAdapter.ts — sendFcmMessage, sendFcmToUser, sendFcmToTopic, validateFcmToken, cleanupInvalidFcmTokens
    Admin messaging accessorlib/firebase-admin.server.ts → getAdminMessaging()
    Token DB layerlib/notifications/fcm-token-db.ts
    RFC sendfeatures/notifications/services/webpush-service.ts
    Client hook / SWhooks/use-fcm.ts, public/firebase-messaging-sw.js

    PostgreSQL: data/migrations/016_fcm_jsonb_schema.sql, 046_push_subscriptions_jsonb.sql.

    Environment

    PurposeVariablesWhere
    Firebase client + FCM subscribeNEXT_PUBLIC_FIREBASE_* + NEXT_PUBLIC_FIREBASE_VAPID_KEYClient / Docker build-arg
    Admin sendAUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY; DB_BACKEND_MODEServer-only
    RFC Web PushVAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECTRuntime Secret — webpush-service.ts

    Troubleshooting

    IssueCauseFix
    token-subscribe-failed / auth credentialCertificate does not belong to this Firebase project, or Cloud Messaging API disabledGenerate Web Push certificates in this project’s Console; rebuild image; enable FCM + Installations APIs
    ConfigMap updated but browser still oldNEXT_PUBLIC_* inlined at buildRebuild/redeploy with matching build-arg
    Permission deniedUser denied notificationsRequest permission before getToken()
    401 on registerNo sessionCookie / Bearer required
    UNREGISTERED on sendStale tokenFCMService / sendFcmToUser marks row invalid
    RFC web-push emptyMissing VAPID_* trio, Firebase-less Safari never prompted, or PushManager already owned by FCMSet dedicated runtime secrets; RFC isSupported does not need NEXT_PUBLIC_FIREBASE_*; iOS = Home Screen PWA. Chrome-only users should have empty push_subscriptions
    Foreground toast + OS banneruseFCMMessages used to call new Notification(), or FCM SW push + onBackgroundMessage both showingFocused tab is toast (or call/game banner) only; FCM SW skips push when messaging init succeeded; data-only for call/game
    validate-fcm-env --format yaml private key ~2 charsOld regex captured `-` as the value
    Call-invite dedupe differs across podsREDIS_URL unset → in-process MapOptional Redis; k3s-or ring-platform-org is 1 replica today
    Disable then push returnsNotification.permission stays granted; init effect used to re-subscribering_push_opt_out localStorage + unregisterCurrentDevicePush

    See-also: how token storage routes by DB_BACKEND_MODE.

    Webrtc Calls

    Same-workflow: CALL_INVITE offline push uses this dual-stack (data-only FCM, 90s TTL).

    Peer Games

    Same-workflow: GAME_REQUEST offline push after Tunnel presence grace.

    RFC dual-stack (Chrome + Safari)
    Dedicated keypair (npx web-push generate-vapid-keys) in runtime Secret as VAPID_* — not the Console certificate
    RFC-only Safari (no Firebase)Set VAPID_* runtime secrets. iPhone/iPad: Add to Home Screen (display: standalone). Chrome still uses FCM when NEXT_PUBLIC_FIREBASE_* is present.

    Operator checklist (shipped)

    1. Firebase project with Cloud Messaging enabled
    2. Web Push certificate generated in that same project → NEXT_PUBLIC_FIREBASE_VAPID_KEY (Docker build-arg)
    3. Server service account → AUTH_FIREBASE_PROJECT_ID / AUTH_FIREBASE_CLIENT_EMAIL / AUTH_FIREBASE_PRIVATE_KEY
    4. HTTPS (or localhost) + service worker at /firebase-messaging-sw.js
    5. Signed-in users (Auth.js) before token registration
    6. After rotating the certificate: rebuild/redeploy — ConfigMap alone does not refresh the browser bundle
    7. RFC (Safari / Firebase-less): VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT as runtime Secret; iOS users must open the Home Screen web app
    8. iPhone/iPad: Share → Add to Home Screen → open the icon → Enable (Safari tabs have no PushManager). Need a real 180×180 /apple-touch-icon.png so iOS saves a web app, not a bookmark
    Store
    fcm_tokens via lib/notifications/fcm-token-db.ts
    push_subscriptions JSONB (migration 046)
    Server sendfcm-service.ts Admin HTTP v1 sendEachwebpush-service.ts (web-push); dual Promise.allSettled on PUSH
    npmfirebase, firebase-adminweb-push

    Env map

    VariableRoleStatus
    NEXT_PUBLIC_FIREBASE_VAPID_KEYConsole Web Push certificate (public) for getToken()Required for FCM — build-arg; read via getFcmVapidKey(). Not needed on RFC-only Safari
    VAPID_PUBLIC_KEYDedicated RFC public for web-push / PushManagerRuntime Secret — must differ from Console cert
    VAPID_PRIVATE_KEYDedicated RFC private (server-only)Runtime Secret — never a build-arg
    VAPID_SUBJECTmailto: or https: contact URIRuntime Secret

    FCM send uses Admin SDK (AUTH_FIREBASE_*), not VAPID_PRIVATE_KEY. isFcmConfigured() is presence + non-placeholder only (validateFirebaseConfig() && validateFcmVapidKey()). RFC-only browsers set isSupported from Push APIs + /api/push/vapid-public, not from Firebase. Mismatched FCM certificates surface as Firebase getToken errors — Ring does not classify key prefixes.

    SurfaceChrome / FCMSafari RFC (macOS tab or iOS Home Screen PWA)
    SubscribegetToken → fcm_tokensPushManager.subscribe only if no existing subscription → push_subscriptions
    Focused tabIn-app toast; CALL_INVITE / GAME_REQUEST → global banners + ringtone (no new Notification(), no navigate)Tunnel banners when the tab is open; SW showNotification (WebKit forbids silent push)
    Hidden / killedFCM SW OS banner (onBackgroundMessage; data-only for call/game)RFC SW OS banner (push-sw.js)
    Missed call TTL~90s (webpush.headers.TTL)~90s (web-push TTL)

    How server PUSH dual-dispatch works

    NotificationChannel.PUSH always calls both Firebase Admin (fcm_tokens) and RFC web-push (push_subscriptions). That is per-user device-class fan-out, not two deliveries to Chrome:

    DeviceTable rowWhat the other stack does
    Chrome (FCM getToken owns PushManager)fcm_tokensRFC subscribe skipped → no push_subscriptions row → web-push attempted: 0 (no-op)
    Safari RFC-onlypush_subscriptionsNo FCM token → Admin attempted: 0 (no-op)
    Same account, Chrome laptop + Safari phonebothboth sends are required

    Do not skip RFC globally when FCM is configured — that would drop the Safari device. Empty push_subscriptions on a Chrome-only user is expected, not a missing second pipe.

    Delivery accounting (lib/notifications/push-dual-dispatch.ts): delivered if either stack sent > 0; failed if endpoints existed but every send failed; noop (still recorded DELIVERED on the PUSH channel) when both tables are empty.

    Build-time vs runtime

    NEXT_PUBLIC_FIREBASE_VAPID_KEY is inlined into the Next.js client bundle at image build. Cluster ConfigMap can update process env, but browsers keep the previous inlined value until rebuild. VAPID_* are runtime-only — rotate without rebuilding the client bundle.

    Token flow

    FCM token register and send

    Prerequisites

    • Auth.js session — server derives user_id
    • HTTPS or localhost; SW at /firebase-messaging-sw.js (FCM) or /push-sw.js (RFC-only)
    • Valid NEXT_PUBLIC_FIREBASE_* + this project’s Console Web Push certificate or RFC-only VAPID_* (Safari / no Firebase)
    • Server Admin: AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY

    Register path

    1. 1

      Stable device fingerprint

      Persist a UUID (e.g. localStorage) and reuse it. Upsert key is (user_id, device_fingerprint).

    2. 2

      Permission + FCM token

      Notification.requestPermission(), then subscribeWithVapid() → Firebase getToken with getFcmVapidKey() (hooks/use-fcm.ts). FCMProviderRuntime owns one useFCM() instance; useFCMMessages(onMessageReceived, isSupported) must not call useFCM again.

    3. 3

      Persist on the server

      POST /api/notifications/fcm/register (avoids Server Action RSC revalidation). Body: token, deviceFingerprint, optional deviceInfo / platform. Auth required.

    4. 4

      Unregister on logout

      useAuth().signOut() runs device unregister. Non-React: DELETE /api/notifications/fcm/register with the same deviceFingerprint.

    Server send (verified modules)

    ModulePath
    Domain FCM send + invalid-token cleanupfeatures/notifications/services/fcm-service.ts
    firebase-full adapter bridgelib/database/adapters/FirebaseAdapter.ts — sendFcmMessage, sendFcmToUser, sendFcmToTopic, validateFcmToken, cleanupInvalidFcmTokens
    Admin messaging accessorlib/firebase-admin.server.ts → getAdminMessaging()
    Token DB layerlib/notifications/fcm-token-db.ts
    RFC sendfeatures/notifications/services/webpush-service.ts
    Client hook / SWhooks/use-fcm.ts, public/firebase-messaging-sw.js

    PostgreSQL: data/migrations/016_fcm_jsonb_schema.sql, 046_push_subscriptions_jsonb.sql.

    Environment

    PurposeVariablesWhere
    Firebase client + FCM subscribeNEXT_PUBLIC_FIREBASE_* + NEXT_PUBLIC_FIREBASE_VAPID_KEYClient / Docker build-arg
    Admin sendAUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY; DB_BACKEND_MODEServer-only
    RFC Web PushVAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECTRuntime Secret — webpush-service.ts

    Troubleshooting

    IssueCauseFix
    token-subscribe-failed / auth credentialCertificate does not belong to this Firebase project, or Cloud Messaging API disabledGenerate Web Push certificates in this project’s Console; rebuild image; enable FCM + Installations APIs
    ConfigMap updated but browser still oldNEXT_PUBLIC_* inlined at buildRebuild/redeploy with matching build-arg
    Permission deniedUser denied notificationsRequest permission before getToken()
    401 on registerNo sessionCookie / Bearer required
    UNREGISTERED on sendStale tokenFCMService / sendFcmToUser marks row invalid
    RFC web-push emptyMissing VAPID_* trio, Firebase-less Safari never prompted, or PushManager already owned by FCMSet dedicated runtime secrets; RFC isSupported does not need NEXT_PUBLIC_FIREBASE_*; iOS = Home Screen PWA. Chrome-only users should have empty push_subscriptions
    Foreground toast + OS banneruseFCMMessages used to call new Notification(), or FCM SW push + onBackgroundMessage both showingFocused tab is toast (or call/game banner) only; FCM SW skips push when messaging init succeeded; data-only for call/game
    validate-fcm-env --format yaml private key ~2 charsOld regex captured `-` as the value
    Call-invite dedupe differs across podsREDIS_URL unset → in-process MapOptional Redis; k3s-or ring-platform-org is 1 replica today
    Disable then push returnsNotification.permission stays granted; init effect used to re-subscribering_push_opt_out localStorage + unregisterCurrentDevicePush

    See-also: how token storage routes by DB_BACKEND_MODE.

    Webrtc Calls

    Same-workflow: CALL_INVITE offline push uses this dual-stack (data-only FCM, 90s TTL).

    Peer Games

    Same-workflow: GAME_REQUEST offline push after Tunnel presence grace.

    1. Docs
    2. /Features
    3. /Push Notifications with FCM (Ring-Powered)

    Updated Aug 16, 20269 min listen

    1. Docs
    2. /Features
    3. /Push Notifications with FCM (Ring-Powered)

    Updated Aug 16, 20269 min listen

    1. Docs
    2. /Features
    3. /Push Notifications with FCM (Ring-Powered)

    Updated Aug 16, 20269 min listen