Documentation

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

    Welcome to Ring
    Quick Reference
    Getting Started
    Prerequisites
    Installation
    First Success Validation
    Next Steps
    Features
    Multi-Vendor Store
    SubscriptionConductor
    PaymentConductor
    Payments Overview
    WayForPay Payment Integration
    Wallet & Credit System
    WalletConductor
    Affiliate & Referral Enablement
    Referral Codes (Refcodes)
    NFT Exhibition Marketplace
    Solana NFT Gates
    Token Staking System
    Owner Project Lab
    Entities
    Opportunities
    Real-Time Messaging
    Ring Tasks
    WebRTC Calls & STUNner TURN
    News Module
    Member Blogs
    Public Profile Pages
    Username Reservation System
    Scientific Editor
    Notifications
    Push Notifications with FCM (Ring-Powered)
    Email AI-CRM
    Ring Mailer & RingdomX Mail
    Tunnel Protocol
    VideoConductor
    MediaConductor
    Generative Gallery
    Authentication
    Security & Compliance
    Admin console
    Admin Wiki
    Manage via Telegram
    Locale System
    Mobile Experience
    Performance Optimization Patterns
    Examples
    Quick Start
    Basic Setup
    White Label
    Custom Branding
    Web3 Integration
    Real World
    Advanced Features
    Customization
    Quick Start — Your First Ring Clone
    Customization Guide
    Branding
    Themes
    Features
    Localization
    Token Economics Setup
    Payment Gateway Integration
    Reference Ring deployments
    Web3
    Token launch jurisdictions
    Wallet
    Wallet Security Tips
    Integrations
    Ethereum wallets (Wagmi v3)
    RingFileBase (object storage API)
    Ring CDN (RingFileBase edge)
    Deployment
    Self-hosted deployment
    Vercel
    Docker
    Environment Configuration
    Monitoring & Analytics
    Performance Optimization
    Backup & Recovery
    Architecture
    Data Model
    Security
    Real Time
    Discovery Mutation Sync
    PaymentConductor architecture
    WalletConductor architecture
    Development
    Ring MCP Server

    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
    ringdom.org — LegioX homebase
    Source — MIT license (GitHub)

    Documentation

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

    Welcome to Ring
    Quick Reference
    Getting Started
    Prerequisites
    Installation
    First Success Validation
    Next Steps
    Features
    Multi-Vendor Store
    SubscriptionConductor
    PaymentConductor
    Payments Overview
    WayForPay Payment Integration
    Wallet & Credit System
    WalletConductor
    Affiliate & Referral Enablement
    Referral Codes (Refcodes)
    NFT Exhibition Marketplace
    Solana NFT Gates
    Token Staking System
    Owner Project Lab
    Entities
    Opportunities
    Real-Time Messaging
    Ring Tasks
    WebRTC Calls & STUNner TURN
    News Module
    Member Blogs
    Public Profile Pages
    Username Reservation System
    Scientific Editor
    Notifications
    Push Notifications with FCM (Ring-Powered)
    Email AI-CRM
    Ring Mailer & RingdomX Mail
    Tunnel Protocol
    VideoConductor
    MediaConductor
    Generative Gallery
    Authentication
    Security & Compliance
    Admin console
    Admin Wiki
    Manage via Telegram
    Locale System
    Mobile Experience
    Performance Optimization Patterns
    Examples
    Quick Start
    Basic Setup
    White Label
    Custom Branding
    Web3 Integration
    Real World
    Advanced Features
    Customization
    Quick Start — Your First Ring Clone
    Customization Guide
    Branding
    Themes
    Features
    Localization
    Token Economics Setup
    Payment Gateway Integration
    Reference Ring deployments
    Web3
    Token launch jurisdictions
    Wallet
    Wallet Security Tips
    Integrations
    Ethereum wallets (Wagmi v3)
    RingFileBase (object storage API)
    Ring CDN (RingFileBase edge)
    Deployment
    Self-hosted deployment
    Vercel
    Docker
    Environment Configuration
    Monitoring & Analytics
    Performance Optimization
    Backup & Recovery
    Architecture
    Data Model
    Security
    Real Time
    Discovery Mutation Sync
    PaymentConductor architecture
    WalletConductor architecture
    Development
    Ring MCP Server

    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
    ringdom.org — LegioX homebase
    Source — MIT license (GitHub)

    Documentation

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

    Welcome to Ring
    Quick Reference
    Getting Started
    Prerequisites
    Installation
    First Success Validation
    Next Steps
    Features
    Multi-Vendor Store
    SubscriptionConductor
    PaymentConductor
    Payments Overview
    WayForPay Payment Integration
    Wallet & Credit System
    WalletConductor
    Affiliate & Referral Enablement
    Referral Codes (Refcodes)
    NFT Exhibition Marketplace
    Solana NFT Gates
    Token Staking System
    Owner Project Lab
    Entities
    Opportunities
    Real-Time Messaging
    Ring Tasks
    WebRTC Calls & STUNner TURN
    News Module
    Member Blogs
    Public Profile Pages
    Username Reservation System
    Scientific Editor
    Notifications
    Push Notifications with FCM (Ring-Powered)
    Email AI-CRM
    Ring Mailer & RingdomX Mail
    Tunnel Protocol
    VideoConductor
    MediaConductor
    Generative Gallery
    Authentication
    Security & Compliance
    Admin console
    Admin Wiki
    Manage via Telegram
    Locale System
    Mobile Experience
    Performance Optimization Patterns
    Examples
    Quick Start
    Basic Setup
    White Label
    Custom Branding
    Web3 Integration
    Real World
    Advanced Features
    Customization
    Quick Start — Your First Ring Clone
    Customization Guide
    Branding
    Themes
    Features
    Localization
    Token Economics Setup
    Payment Gateway Integration
    Reference Ring deployments
    Web3
    Token launch jurisdictions
    Wallet
    Wallet Security Tips
    Integrations
    Ethereum wallets (Wagmi v3)
    RingFileBase (object storage API)
    Ring CDN (RingFileBase edge)
    Deployment
    Self-hosted deployment
    Vercel
    Docker
    Environment Configuration
    Monitoring & Analytics
    Performance Optimization
    Backup & Recovery
    Architecture
    Data Model
    Security
    Real Time
    Discovery Mutation Sync
    PaymentConductor architecture
    WalletConductor architecture
    Development
    Ring MCP Server

    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
    ringdom.org — LegioX homebase
    Source — MIT license (GitHub)
    1. Docs
    2. /Features
    3. /Push Notifications with FCM (Ring-Powered)

    Updated Jul 1, 20268 min listen

    Ring Platform Logo

    Завантаження документації...

    Підготовка контенту платформи Ring

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

    Updated Jul 1, 20268 min listen

    Ring Platform Logo

    Завантаження документації...

    Підготовка контенту платформи Ring

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

    Updated Jul 1, 20268 min listen

    Ring Platform Logo

    Завантаження документації...

    Підготовка контенту платформи Ring

    Push Notifications with FCM (Ring-Powered)

    Use the Founder / Developer tabs to filter this page for your role. FCM is available in all DB_BACKEND_MODE values (k8s-postgres-fcm, firebase-full, supabase-fcm) — you never need Firestore for push alone.

    Ring Platform stores FCM tokens in your primary database (PostgreSQL or Firestore by backend mode), one row per user per device. React apps register tokens via a Server Action; non-React clients use a rate-limited API. Both paths call the same server-only DB layer so behavior and security stay consistent everywhere.

    Backend modes

    Notifications API

    useAuth sign-out

    Firebase integration

    What Ring-powered FCM gives you

    • One code path, any backend — your FCM tokens live in your primary database (PostgreSQL or Firestore), not a separate Firebase service
    • One row per device per user — a user who signs in on their phone, laptop, and tablet gets exactly 3 token rows, not a growing list of stale tokens
    • Self-healing delivery — invalid tokens are automatically cleaned up when FCM reports them as unregistered (no waste, no quota burn)
    • Server Action token registration — React apps call a Server Action; the server derives user_id from the authenticated session (never from the client)

    How it works

    Ring-powered FCM token flow

    What you need

    • Authentication — users must be signed in (Auth.js session). Token registration is always tied to the authenticated user
    • HTTPS (or localhost) — required for Web Push in browsers
    • Firebase project — with Cloud Messaging enabled, and a VAPID key pair generated
    • No Firestore required for push — in k8s-postgres-fcm and supabase-fcm modes, FCM works with PostgreSQL only

    Overview: Ring-Powered FCM Logic

    "Ring-powered" FCM means a single, backend-agnostic design:

    • Token store as source of truth — The fcm_tokens table (PostgreSQL) or collection (Firestore) holds all registration tokens. Stored payload lives in a JSONB data column where applicable. No secondary cache; the database is the registry.
    • One row per device per user — A composite unique constraint on (user_id, device_fingerprint) ensures one record per device. The token value is not unique, so FCM token rotation updates the same row instead of creating duplicates.
    • React app → Server Action — The primary path for browser and React Native web is the upsertFcmToken Server Action. The server derives user_id from auth() only; the client never sends it.
    • Non-React clients → API route — Mobile apps or external services call POST /api/notifications/fcm/register with the same payload. This endpoint is rate-limited per user (e.g. 10 requests per minute).
    • Server-side send → FirebaseAdapter FCM bridge — The new sendFcmToUser(), sendFcmToTopic(), validateFcmToken(), and cleanupInvalidFcmTokens() methods on FirebaseAdapter provide a typed, reactive-cleanup-aware send surface. All use the HTTP v1 API exclusively.
    • Lifecycle in data — Each row carries status (active | stale | invalid) and invalidatedAt. Reactive cleanup runs when FCM returns errors for a token; scheduled jobs mark stale tokens and optionally hard-delete after a retention window.
    • Backend mode — Token storage is backend-agnostic. BackendSelector routes by DB_BACKEND_MODE to PostgreSQL (k8s-postgres-fcm, supabase-fcm) or Firestore (firebase-full). One code path; no branching in the Server Action or API route.
    Ring-powered FCM registration and delivery

    Push Notifications with FCM (Ring-Powered)

    Use the Founder / Developer tabs to filter this page for your role. FCM is available in all DB_BACKEND_MODE values (k8s-postgres-fcm, firebase-full, supabase-fcm) — you never need Firestore for push alone.

    Ring Platform stores FCM tokens in your primary database (PostgreSQL or Firestore by backend mode), one row per user per device. React apps register tokens via a Server Action; non-React clients use a rate-limited API. Both paths call the same server-only DB layer so behavior and security stay consistent everywhere.

    Backend modes

    Notifications API

    useAuth sign-out

    Firebase integration

    What Ring-powered FCM gives you

    • One code path, any backend — your FCM tokens live in your primary database (PostgreSQL or Firestore), not a separate Firebase service
    • One row per device per user — a user who signs in on their phone, laptop, and tablet gets exactly 3 token rows, not a growing list of stale tokens
    • Self-healing delivery — invalid tokens are automatically cleaned up when FCM reports them as unregistered (no waste, no quota burn)
    • Server Action token registration — React apps call a Server Action; the server derives user_id from the authenticated session (never from the client)

    How it works

    Ring-powered FCM token flow

    What you need

    • Authentication — users must be signed in (Auth.js session). Token registration is always tied to the authenticated user
    • HTTPS (or localhost) — required for Web Push in browsers
    • Firebase project — with Cloud Messaging enabled, and a VAPID key pair generated
    • No Firestore required for push — in k8s-postgres-fcm and supabase-fcm modes, FCM works with PostgreSQL only

    Overview: Ring-Powered FCM Logic

    "Ring-powered" FCM means a single, backend-agnostic design:

    • Token store as source of truth — The fcm_tokens table (PostgreSQL) or collection (Firestore) holds all registration tokens. Stored payload lives in a JSONB data column where applicable. No secondary cache; the database is the registry.
    • One row per device per user — A composite unique constraint on (user_id, device_fingerprint) ensures one record per device. The token value is not unique, so FCM token rotation updates the same row instead of creating duplicates.
    • React app → Server Action — The primary path for browser and React Native web is the upsertFcmToken Server Action. The server derives user_id from auth() only; the client never sends it.
    • Non-React clients → API route — Mobile apps or external services call POST /api/notifications/fcm/register with the same payload. This endpoint is rate-limited per user (e.g. 10 requests per minute).
    • Server-side send → FirebaseAdapter FCM bridge — The new sendFcmToUser(), sendFcmToTopic(), validateFcmToken(), and cleanupInvalidFcmTokens() methods on FirebaseAdapter provide a typed, reactive-cleanup-aware send surface. All use the HTTP v1 API exclusively.
    • Lifecycle in data — Each row carries status (active | stale | invalid) and invalidatedAt. Reactive cleanup runs when FCM returns errors for a token; scheduled jobs mark stale tokens and optionally hard-delete after a retention window.
    • Backend mode — Token storage is backend-agnostic. BackendSelector routes by DB_BACKEND_MODE to PostgreSQL (k8s-postgres-fcm, supabase-fcm) or Firestore (firebase-full). One code path; no branching in the Server Action or API route.
    Ring-powered FCM registration and delivery

    Push Notifications with FCM (Ring-Powered)

    Use the Founder / Developer tabs to filter this page for your role. FCM is available in all DB_BACKEND_MODE values (k8s-postgres-fcm, firebase-full, supabase-fcm) — you never need Firestore for push alone.

    Ring Platform stores FCM tokens in your primary database (PostgreSQL or Firestore by backend mode), one row per user per device. React apps register tokens via a Server Action; non-React clients use a rate-limited API. Both paths call the same server-only DB layer so behavior and security stay consistent everywhere.

    Backend modes

    Notifications API

    useAuth sign-out

    Firebase integration

    What Ring-powered FCM gives you

    • One code path, any backend — your FCM tokens live in your primary database (PostgreSQL or Firestore), not a separate Firebase service
    • One row per device per user — a user who signs in on their phone, laptop, and tablet gets exactly 3 token rows, not a growing list of stale tokens
    • Self-healing delivery — invalid tokens are automatically cleaned up when FCM reports them as unregistered (no waste, no quota burn)
    • Server Action token registration — React apps call a Server Action; the server derives user_id from the authenticated session (never from the client)

    How it works

    Ring-powered FCM token flow

    What you need

    • Authentication — users must be signed in (Auth.js session). Token registration is always tied to the authenticated user
    • HTTPS (or localhost) — required for Web Push in browsers
    • Firebase project — with Cloud Messaging enabled, and a VAPID key pair generated
    • No Firestore required for push — in k8s-postgres-fcm and supabase-fcm modes, FCM works with PostgreSQL only

    Overview: Ring-Powered FCM Logic

    "Ring-powered" FCM means a single, backend-agnostic design:

    • Token store as source of truth — The fcm_tokens table (PostgreSQL) or collection (Firestore) holds all registration tokens. Stored payload lives in a JSONB data column where applicable. No secondary cache; the database is the registry.
    • One row per device per user — A composite unique constraint on (user_id, device_fingerprint) ensures one record per device. The token value is not unique, so FCM token rotation updates the same row instead of creating duplicates.
    • React app → Server Action — The primary path for browser and React Native web is the upsertFcmToken Server Action. The server derives user_id from auth() only; the client never sends it.
    • Non-React clients → API route — Mobile apps or external services call POST /api/notifications/fcm/register with the same payload. This endpoint is rate-limited per user (e.g. 10 requests per minute).
    • Server-side send → FirebaseAdapter FCM bridge — The new sendFcmToUser(), sendFcmToTopic(), validateFcmToken(), and cleanupInvalidFcmTokens() methods on FirebaseAdapter provide a typed, reactive-cleanup-aware send surface. All use the HTTP v1 API exclusively.
    • Lifecycle in data — Each row carries status (active | stale | invalid) and invalidatedAt. Reactive cleanup runs when FCM returns errors for a token; scheduled jobs mark stale tokens and optionally hard-delete after a retention window.
    • Backend mode — Token storage is backend-agnostic. BackendSelector routes by DB_BACKEND_MODE to PostgreSQL (k8s-postgres-fcm, supabase-fcm) or Firestore (firebase-full). One code path; no branching in the Server Action or API route.
    Ring-powered FCM registration and delivery

    Prerequisites

    • Authentication — A signed-in user (Auth.js session). Token registration is always tied to the authenticated user; the server never trusts a client-supplied user id.
    • Web push (browser) — HTTPS (or localhost for development), browser support for Web Push, a registered service worker, and the Firebase client SDK. The service worker file is served at /firebase-messaging-sw.js.
    • API clients — Valid session (cookie or Bearer) and the same request body contract as below (token, deviceFingerprint, deviceInfo).
    • FCM Admin SDK — Available in all DB_BACKEND_MODE values. No Firestore needed for push alone.

    React App: Server Action Path

    Use the Server Action as the primary way to register and refresh FCM tokens from any React or Next.js client. This path is not rate-limited like the API route and keeps user id server-derived only.

    Primary path

    React apps should use the Server Action as the primary path. The API route is for non-React clients and is rate-limited.

    1. 1

      Obtain a stable device fingerprint

      Generate or load a stable value (for example, crypto.randomUUID() stored in localStorage) and reuse it on every load and on token refresh. The server uses (user_id, device_fingerprint) to upsert one row per device.

    2. 2

      Request permission and get the FCM token

      In a 'use client' component (for example inside a notification provider), call Notification.requestPermission(). When granted, call getToken(messaging, { vapidKey }) from the Firebase Messaging SDK. Do not call getToken() in Server Components; they have no browser context.

    3. 3

      Call the Server Action to register the token

      Invoke upsertFcmToken({ token, deviceFingerprint, deviceInfo, platform: 'web' }). Do not pass userId; the server derives it from auth().

    4. 4

      Handle token refresh

      When the Firebase SDK emits a new token (for example via onTokenRefresh), call the same Server Action again with the new token and the same deviceFingerprint. The shared DB layer updates the existing row for that user and device.

    Example: FCMProvider wraps useFCM in the app shell — prefer that over ad-hoc copies.

    Non-React Clients: API Route

    Mobile apps or other non-React clients register tokens by calling the same contract over HTTP.

    Endpoint: POST /api/notifications/fcm/register

    Request body:

    • token (string, required) — FCM registration token from the client SDK.
    • deviceFingerprint (string, required) — Stable device identifier (e.g. UUID in local storage). Maximum 128 characters; alphanumeric, hyphens, and underscores only.
    • deviceInfo (object, optional) — Arbitrary device metadata (e.g. platform, userAgent).

    Authentication: Required (session cookie or Bearer token). The server uses the authenticated user id; do not send userId in the body.

    Responses:

    • 200 — { "success": true }
    • 400 — Validation error (e.g. missing or invalid deviceFingerprint)
    • 401 — Not authenticated
    • 429 — Rate limit exceeded (see Retry-After header)
    • 500 — Server error
    Rate limit

    This endpoint is rate-limited per user (e.g. 10 requests per minute). Use the Server Action from React apps to avoid consuming the API rate limit.

    Example with cURL (replace session cookie or use Bearer token as required by your auth setup):

    Sending Push Messages with the Adapter FCM Bridge (new)

    In firebase-full mode, the FirebaseAdapter now exposes dedicated FCM Admin bridge methods that use the HTTP v1 API exclusively (sendEach() not legacy sendMulticast()):

    fcm_tokens collection model (Firestore)

    When running in firebase-full mode, the fcm_tokens Firestore collection has this schema:

    For the equivalent PostgreSQL schema (k8s-postgres-fcm / supabase-fcm modes), see data/migrations/016_fcm_jsonb_schema.sql and the FCM specialist truth lens.

    Unregister and Logout

    On logout or when the user disables push:

    • Browser / React — Call useAuth().signOut(). It runs unregisterCurrentDeviceFcmToken() (Firebase deleteToken + server invalidation by deviceFingerprint) before Auth.js sign-out. FCMProvider also exposes disable flows via fcm-provider.tsx.
    • Manual disable — DELETE /api/notifications/fcm/register with body { "deviceFingerprint": "same-uuid-as-register" } for non-React clients.
    • Non-React — Same DELETE request with the same deviceFingerprint used at registration.

    The server marks the row as status: 'invalid' and sets invalidatedAt; delivery to that token is stopped.

    Environment Variables

    Split configuration so that only public, safe values are exposed to the client.

    Security

    VAPID private key and Firebase service account credentials must never be exposed to the client. Keep them server-only.

    PurposeVariablesWhere
    Client (browser / app)NEXT_PUBLIC_FIREBASE_API_KEY, NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, NEXT_PUBLIC_FIREBASE_PROJECT_ID, NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, NEXT_PUBLIC_FIREBASE_APP_ID, NEXT_PUBLIC_FIREBASE_VAPID_KEYClient-safe; used by Firebase client SDK and service worker
    Server (token store, FCM send)AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY (or ADC on Cloud Run); DB_BACKEND_MODE (e.g. k8s-postgres-fcm, firebase-full, supabase-fcm)Server-only; used by getAdminMessaging() and BackendSelector

    Troubleshooting

    IssueCauseWhat to do
    Permission denied or token not receivedUser denied notification permission or permission not yet requestedRequest permission before calling getToken(); handle permission === 'denied' in the UI
    429 on registerRate limit exceededUse the Server Action from React apps; for API clients, respect Retry-After and reduce register frequency
    Invalid deviceFingerprintValidation failureUse a string of length 1–128 with only letters, numbers, hyphens, and underscores
    401 on registerNot authenticatedEnsure a valid session (cookie or Bearer) is sent; do not send userId in the body
    UNREGISTERED on sendToken is stale / invalidsendFcmToUser auto-handles this via cleanupInvalidFcmTokens — no manual action needed
    sendMulticast deprecationLegacy API calledUse sendEach() from the Admin SDK (not sendMulticast() or sendAll()) — this is what sendFcmToUser uses
    Service worker not receivingSW not at root scopeEnsure public/firebase-messaging-sw.js is served at /firebase-messaging-sw.js; register with root scope

    See Also

    Notifications

    Notifications API

    Firebase integration

    k8s-postgres-fcm mode

    Backend modes

    Code structure

    Schema and migrations: data/migrations/016_fcm_jsonb_schema.sql (PostgreSQL) — FCM tokens with JSONB payload, composite unique index, status lifecycle.

    Truth lenses:

    • AI-LEGIOX/legiox-truth-lens/fcm-specialist.nodus.json — Canonical FCM truth: token lifecycle, HTTP v1-only sendEach() pattern, reactive cleanup, pro-active staleness, PostgreSQL token store schema, Next.js 16 App Router integration. See also Firebase integration.
    • AI-LEGIOX/legiox-truth-lens/ring-backend-administrator.nodus.json — firebase-full ring-db mode SSOT context: fcm_tokens collection in Firestore, FirebaseAdapter FCM bridge methods.
    • AI-LEGIOX/legiox-truth-lens/google-firebase-specialist.nodus.json — Firebase 14 Enterprise AI Logic integration.
    typescript
    
    import { getDatabaseService } from '@/lib/database'
    import type { FirebaseAdapter } from '@/lib/database/adapters/FirebaseAdapter'
    
    const db = getDatabaseService()
    // If the adapter is the FirebaseAdapter (firebase-full mode):
    
    // 1. Send a push to a single device token
    await adapter.sendFcmMessage({
      token: 'fcm-device-token',
      notification: { title: 'Welcome!', body: 'Your account is ready.' },
      webpush: { fcmOptions: { link: 'https://your-app.com/dashboard' } },
    })
    
    // 2. Send to all active tokens for a user (auto-fetches tokens from fcm_tokens)
    const result = await adapter.sendFcmToUser(userId, {
      title: 'New message',
      body: 'Alice replied to your comment',
    }, { route: '/messages/123' })
    
    // sendFcmToUser automatically handles:
    // - Fetching all active tokens for the user from fcm_tokens
    // - Building a message per token
    // - Calling messaging.sendEach() (HTTP v1, concurrency-safe)
    // - Reactive cleanup: UNREGISTERED / INVALID_ARGUMENT errors
    //   mark tokens as status='invalid' in the fcm_tokens collection
    
    // 3. Send to a topic (all subscribers)
    await adapter.sendFcmToTopic('global-announcements', {
      title: 'Scheduled maintenance',
      body: 'Platform will be unavailable 2–4 AM UTC',
    })
    
    // 4. Validate a token's liveness
    const { data } = await adapter.validateFcmToken(token)
    if (data?.valid === false) {
      await adapter.cleanupInvalidFcmTokens([token])
    }
    
    // 5. Bulk invalidate dead tokens
    await adapter.cleanupInvalidFcmTokens(['dead-token-1', 'dead-token-2'])
    typescript
    
    interface FcmTokenRow {
      user_id: string           // Auth.js user ID (server-derived, never from client)
      token: string             // FCM registration token (variable length, ~140-180 chars)
      device_fingerprint: string // Stable device identifier (UUID)
      platform: string          // 'web' | 'android' | 'ios'
      status: 'active' | 'stale' | 'invalid'
      created_at: Timestamp
      last_seen_at: Timestamp   // Updated on every successful delivery
      invalidated_at?: Timestamp // Set when status becomes 'invalid'
    }

    Prerequisites

    • Authentication — A signed-in user (Auth.js session). Token registration is always tied to the authenticated user; the server never trusts a client-supplied user id.
    • Web push (browser) — HTTPS (or localhost for development), browser support for Web Push, a registered service worker, and the Firebase client SDK. The service worker file is served at /firebase-messaging-sw.js.
    • API clients — Valid session (cookie or Bearer) and the same request body contract as below (token, deviceFingerprint, deviceInfo).
    • FCM Admin SDK — Available in all DB_BACKEND_MODE values. No Firestore needed for push alone.

    React App: Server Action Path

    Use the Server Action as the primary way to register and refresh FCM tokens from any React or Next.js client. This path is not rate-limited like the API route and keeps user id server-derived only.

    Primary path

    React apps should use the Server Action as the primary path. The API route is for non-React clients and is rate-limited.

    1. 1

      Obtain a stable device fingerprint

      Generate or load a stable value (for example, crypto.randomUUID() stored in localStorage) and reuse it on every load and on token refresh. The server uses (user_id, device_fingerprint) to upsert one row per device.

    2. 2

      Request permission and get the FCM token

      In a 'use client' component (for example inside a notification provider), call Notification.requestPermission(). When granted, call getToken(messaging, { vapidKey }) from the Firebase Messaging SDK. Do not call getToken() in Server Components; they have no browser context.

    3. 3

      Call the Server Action to register the token

      Invoke upsertFcmToken({ token, deviceFingerprint, deviceInfo, platform: 'web' }). Do not pass userId; the server derives it from auth().

    4. 4

      Handle token refresh

      When the Firebase SDK emits a new token (for example via onTokenRefresh), call the same Server Action again with the new token and the same deviceFingerprint. The shared DB layer updates the existing row for that user and device.

    Example: FCMProvider wraps useFCM in the app shell — prefer that over ad-hoc copies.

    Non-React Clients: API Route

    Mobile apps or other non-React clients register tokens by calling the same contract over HTTP.

    Endpoint: POST /api/notifications/fcm/register

    Request body:

    • token (string, required) — FCM registration token from the client SDK.
    • deviceFingerprint (string, required) — Stable device identifier (e.g. UUID in local storage). Maximum 128 characters; alphanumeric, hyphens, and underscores only.
    • deviceInfo (object, optional) — Arbitrary device metadata (e.g. platform, userAgent).

    Authentication: Required (session cookie or Bearer token). The server uses the authenticated user id; do not send userId in the body.

    Responses:

    • 200 — { "success": true }
    • 400 — Validation error (e.g. missing or invalid deviceFingerprint)
    • 401 — Not authenticated
    • 429 — Rate limit exceeded (see Retry-After header)
    • 500 — Server error
    Rate limit

    This endpoint is rate-limited per user (e.g. 10 requests per minute). Use the Server Action from React apps to avoid consuming the API rate limit.

    Example with cURL (replace session cookie or use Bearer token as required by your auth setup):

    Sending Push Messages with the Adapter FCM Bridge (new)

    In firebase-full mode, the FirebaseAdapter now exposes dedicated FCM Admin bridge methods that use the HTTP v1 API exclusively (sendEach() not legacy sendMulticast()):

    fcm_tokens collection model (Firestore)

    When running in firebase-full mode, the fcm_tokens Firestore collection has this schema:

    For the equivalent PostgreSQL schema (k8s-postgres-fcm / supabase-fcm modes), see data/migrations/016_fcm_jsonb_schema.sql and the FCM specialist truth lens.

    Unregister and Logout

    On logout or when the user disables push:

    • Browser / React — Call useAuth().signOut(). It runs unregisterCurrentDeviceFcmToken() (Firebase deleteToken + server invalidation by deviceFingerprint) before Auth.js sign-out. FCMProvider also exposes disable flows via fcm-provider.tsx.
    • Manual disable — DELETE /api/notifications/fcm/register with body { "deviceFingerprint": "same-uuid-as-register" } for non-React clients.
    • Non-React — Same DELETE request with the same deviceFingerprint used at registration.

    The server marks the row as status: 'invalid' and sets invalidatedAt; delivery to that token is stopped.

    Environment Variables

    Split configuration so that only public, safe values are exposed to the client.

    Security

    VAPID private key and Firebase service account credentials must never be exposed to the client. Keep them server-only.

    PurposeVariablesWhere
    Client (browser / app)NEXT_PUBLIC_FIREBASE_API_KEY, NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, NEXT_PUBLIC_FIREBASE_PROJECT_ID, NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, NEXT_PUBLIC_FIREBASE_APP_ID, NEXT_PUBLIC_FIREBASE_VAPID_KEYClient-safe; used by Firebase client SDK and service worker
    Server (token store, FCM send)AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY (or ADC on Cloud Run); DB_BACKEND_MODE (e.g. k8s-postgres-fcm, firebase-full, supabase-fcm)Server-only; used by getAdminMessaging() and BackendSelector

    Troubleshooting

    IssueCauseWhat to do
    Permission denied or token not receivedUser denied notification permission or permission not yet requestedRequest permission before calling getToken(); handle permission === 'denied' in the UI
    429 on registerRate limit exceededUse the Server Action from React apps; for API clients, respect Retry-After and reduce register frequency
    Invalid deviceFingerprintValidation failureUse a string of length 1–128 with only letters, numbers, hyphens, and underscores
    401 on registerNot authenticatedEnsure a valid session (cookie or Bearer) is sent; do not send userId in the body
    UNREGISTERED on sendToken is stale / invalidsendFcmToUser auto-handles this via cleanupInvalidFcmTokens — no manual action needed
    sendMulticast deprecationLegacy API calledUse sendEach() from the Admin SDK (not sendMulticast() or sendAll()) — this is what sendFcmToUser uses
    Service worker not receivingSW not at root scopeEnsure public/firebase-messaging-sw.js is served at /firebase-messaging-sw.js; register with root scope

    See Also

    Notifications

    Notifications API

    Firebase integration

    k8s-postgres-fcm mode

    Backend modes

    Code structure

    Schema and migrations: data/migrations/016_fcm_jsonb_schema.sql (PostgreSQL) — FCM tokens with JSONB payload, composite unique index, status lifecycle.

    Truth lenses:

    • AI-LEGIOX/legiox-truth-lens/fcm-specialist.nodus.json — Canonical FCM truth: token lifecycle, HTTP v1-only sendEach() pattern, reactive cleanup, pro-active staleness, PostgreSQL token store schema, Next.js 16 App Router integration. See also Firebase integration.
    • AI-LEGIOX/legiox-truth-lens/ring-backend-administrator.nodus.json — firebase-full ring-db mode SSOT context: fcm_tokens collection in Firestore, FirebaseAdapter FCM bridge methods.
    • AI-LEGIOX/legiox-truth-lens/google-firebase-specialist.nodus.json — Firebase 14 Enterprise AI Logic integration.
    typescript
    
    import { getDatabaseService } from '@/lib/database'
    import type { FirebaseAdapter } from '@/lib/database/adapters/FirebaseAdapter'
    
    const db = getDatabaseService()
    // If the adapter is the FirebaseAdapter (firebase-full mode):
    
    // 1. Send a push to a single device token
    await adapter.sendFcmMessage({
      token: 'fcm-device-token',
      notification: { title: 'Welcome!', body: 'Your account is ready.' },
      webpush: { fcmOptions: { link: 'https://your-app.com/dashboard' } },
    })
    
    // 2. Send to all active tokens for a user (auto-fetches tokens from fcm_tokens)
    const result = await adapter.sendFcmToUser(userId, {
      title: 'New message',
      body: 'Alice replied to your comment',
    }, { route: '/messages/123' })
    
    // sendFcmToUser automatically handles:
    // - Fetching all active tokens for the user from fcm_tokens
    // - Building a message per token
    // - Calling messaging.sendEach() (HTTP v1, concurrency-safe)
    // - Reactive cleanup: UNREGISTERED / INVALID_ARGUMENT errors
    //   mark tokens as status='invalid' in the fcm_tokens collection
    
    // 3. Send to a topic (all subscribers)
    await adapter.sendFcmToTopic('global-announcements', {
      title: 'Scheduled maintenance',
      body: 'Platform will be unavailable 2–4 AM UTC',
    })
    
    // 4. Validate a token's liveness
    const { data } = await adapter.validateFcmToken(token)
    if (data?.valid === false) {
      await adapter.cleanupInvalidFcmTokens([token])
    }
    
    // 5. Bulk invalidate dead tokens
    await adapter.cleanupInvalidFcmTokens(['dead-token-1', 'dead-token-2'])
    typescript
    
    interface FcmTokenRow {
      user_id: string           // Auth.js user ID (server-derived, never from client)
      token: string             // FCM registration token (variable length, ~140-180 chars)
      device_fingerprint: string // Stable device identifier (UUID)
      platform: string          // 'web' | 'android' | 'ios'
      status: 'active' | 'stale' | 'invalid'
      created_at: Timestamp
      last_seen_at: Timestamp   // Updated on every successful delivery
      invalidated_at?: Timestamp // Set when status becomes 'invalid'
    }

    Prerequisites

    • Authentication — A signed-in user (Auth.js session). Token registration is always tied to the authenticated user; the server never trusts a client-supplied user id.
    • Web push (browser) — HTTPS (or localhost for development), browser support for Web Push, a registered service worker, and the Firebase client SDK. The service worker file is served at /firebase-messaging-sw.js.
    • API clients — Valid session (cookie or Bearer) and the same request body contract as below (token, deviceFingerprint, deviceInfo).
    • FCM Admin SDK — Available in all DB_BACKEND_MODE values. No Firestore needed for push alone.

    React App: Server Action Path

    Use the Server Action as the primary way to register and refresh FCM tokens from any React or Next.js client. This path is not rate-limited like the API route and keeps user id server-derived only.

    Primary path

    React apps should use the Server Action as the primary path. The API route is for non-React clients and is rate-limited.

    1. 1

      Obtain a stable device fingerprint

      Generate or load a stable value (for example, crypto.randomUUID() stored in localStorage) and reuse it on every load and on token refresh. The server uses (user_id, device_fingerprint) to upsert one row per device.

    2. 2

      Request permission and get the FCM token

      In a 'use client' component (for example inside a notification provider), call Notification.requestPermission(). When granted, call getToken(messaging, { vapidKey }) from the Firebase Messaging SDK. Do not call getToken() in Server Components; they have no browser context.

    3. 3

      Call the Server Action to register the token

      Invoke upsertFcmToken({ token, deviceFingerprint, deviceInfo, platform: 'web' }). Do not pass userId; the server derives it from auth().

    4. 4

      Handle token refresh

      When the Firebase SDK emits a new token (for example via onTokenRefresh), call the same Server Action again with the new token and the same deviceFingerprint. The shared DB layer updates the existing row for that user and device.

    Example: FCMProvider wraps useFCM in the app shell — prefer that over ad-hoc copies.

    Non-React Clients: API Route

    Mobile apps or other non-React clients register tokens by calling the same contract over HTTP.

    Endpoint: POST /api/notifications/fcm/register

    Request body:

    • token (string, required) — FCM registration token from the client SDK.
    • deviceFingerprint (string, required) — Stable device identifier (e.g. UUID in local storage). Maximum 128 characters; alphanumeric, hyphens, and underscores only.
    • deviceInfo (object, optional) — Arbitrary device metadata (e.g. platform, userAgent).

    Authentication: Required (session cookie or Bearer token). The server uses the authenticated user id; do not send userId in the body.

    Responses:

    • 200 — { "success": true }
    • 400 — Validation error (e.g. missing or invalid deviceFingerprint)
    • 401 — Not authenticated
    • 429 — Rate limit exceeded (see Retry-After header)
    • 500 — Server error
    Rate limit

    This endpoint is rate-limited per user (e.g. 10 requests per minute). Use the Server Action from React apps to avoid consuming the API rate limit.

    Example with cURL (replace session cookie or use Bearer token as required by your auth setup):

    Sending Push Messages with the Adapter FCM Bridge (new)

    In firebase-full mode, the FirebaseAdapter now exposes dedicated FCM Admin bridge methods that use the HTTP v1 API exclusively (sendEach() not legacy sendMulticast()):

    fcm_tokens collection model (Firestore)

    When running in firebase-full mode, the fcm_tokens Firestore collection has this schema:

    For the equivalent PostgreSQL schema (k8s-postgres-fcm / supabase-fcm modes), see data/migrations/016_fcm_jsonb_schema.sql and the FCM specialist truth lens.

    Unregister and Logout

    On logout or when the user disables push:

    • Browser / React — Call useAuth().signOut(). It runs unregisterCurrentDeviceFcmToken() (Firebase deleteToken + server invalidation by deviceFingerprint) before Auth.js sign-out. FCMProvider also exposes disable flows via fcm-provider.tsx.
    • Manual disable — DELETE /api/notifications/fcm/register with body { "deviceFingerprint": "same-uuid-as-register" } for non-React clients.
    • Non-React — Same DELETE request with the same deviceFingerprint used at registration.

    The server marks the row as status: 'invalid' and sets invalidatedAt; delivery to that token is stopped.

    Environment Variables

    Split configuration so that only public, safe values are exposed to the client.

    Security

    VAPID private key and Firebase service account credentials must never be exposed to the client. Keep them server-only.

    PurposeVariablesWhere
    Client (browser / app)NEXT_PUBLIC_FIREBASE_API_KEY, NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, NEXT_PUBLIC_FIREBASE_PROJECT_ID, NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, NEXT_PUBLIC_FIREBASE_APP_ID, NEXT_PUBLIC_FIREBASE_VAPID_KEYClient-safe; used by Firebase client SDK and service worker
    Server (token store, FCM send)AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY (or ADC on Cloud Run); DB_BACKEND_MODE (e.g. k8s-postgres-fcm, firebase-full, supabase-fcm)Server-only; used by getAdminMessaging() and BackendSelector

    Troubleshooting

    IssueCauseWhat to do
    Permission denied or token not receivedUser denied notification permission or permission not yet requestedRequest permission before calling getToken(); handle permission === 'denied' in the UI
    429 on registerRate limit exceededUse the Server Action from React apps; for API clients, respect Retry-After and reduce register frequency
    Invalid deviceFingerprintValidation failureUse a string of length 1–128 with only letters, numbers, hyphens, and underscores
    401 on registerNot authenticatedEnsure a valid session (cookie or Bearer) is sent; do not send userId in the body
    UNREGISTERED on sendToken is stale / invalidsendFcmToUser auto-handles this via cleanupInvalidFcmTokens — no manual action needed
    sendMulticast deprecationLegacy API calledUse sendEach() from the Admin SDK (not sendMulticast() or sendAll()) — this is what sendFcmToUser uses
    Service worker not receivingSW not at root scopeEnsure public/firebase-messaging-sw.js is served at /firebase-messaging-sw.js; register with root scope

    See Also

    Notifications

    Notifications API

    Firebase integration

    k8s-postgres-fcm mode

    Backend modes

    Code structure

    Schema and migrations: data/migrations/016_fcm_jsonb_schema.sql (PostgreSQL) — FCM tokens with JSONB payload, composite unique index, status lifecycle.

    Truth lenses:

    • AI-LEGIOX/legiox-truth-lens/fcm-specialist.nodus.json — Canonical FCM truth: token lifecycle, HTTP v1-only sendEach() pattern, reactive cleanup, pro-active staleness, PostgreSQL token store schema, Next.js 16 App Router integration. See also Firebase integration.
    • AI-LEGIOX/legiox-truth-lens/ring-backend-administrator.nodus.json — firebase-full ring-db mode SSOT context: fcm_tokens collection in Firestore, FirebaseAdapter FCM bridge methods.
    • AI-LEGIOX/legiox-truth-lens/google-firebase-specialist.nodus.json — Firebase 14 Enterprise AI Logic integration.
    typescript
    
    import { getDatabaseService } from '@/lib/database'
    import type { FirebaseAdapter } from '@/lib/database/adapters/FirebaseAdapter'
    
    const db = getDatabaseService()
    // If the adapter is the FirebaseAdapter (firebase-full mode):
    
    // 1. Send a push to a single device token
    await adapter.sendFcmMessage({
      token: 'fcm-device-token',
      notification: { title: 'Welcome!', body: 'Your account is ready.' },
      webpush: { fcmOptions: { link: 'https://your-app.com/dashboard' } },
    })
    
    // 2. Send to all active tokens for a user (auto-fetches tokens from fcm_tokens)
    const result = await adapter.sendFcmToUser(userId, {
      title: 'New message',
      body: 'Alice replied to your comment',
    }, { route: '/messages/123' })
    
    // sendFcmToUser automatically handles:
    // - Fetching all active tokens for the user from fcm_tokens
    // - Building a message per token
    // - Calling messaging.sendEach() (HTTP v1, concurrency-safe)
    // - Reactive cleanup: UNREGISTERED / INVALID_ARGUMENT errors
    //   mark tokens as status='invalid' in the fcm_tokens collection
    
    // 3. Send to a topic (all subscribers)
    await adapter.sendFcmToTopic('global-announcements', {
      title: 'Scheduled maintenance',
      body: 'Platform will be unavailable 2–4 AM UTC',
    })
    
    // 4. Validate a token's liveness
    const { data } = await adapter.validateFcmToken(token)
    if (data?.valid === false) {
      await adapter.cleanupInvalidFcmTokens([token])
    }
    
    // 5. Bulk invalidate dead tokens
    await adapter.cleanupInvalidFcmTokens(['dead-token-1', 'dead-token-2'])
    typescript
    
    interface FcmTokenRow {
      user_id: string           // Auth.js user ID (server-derived, never from client)
      token: string             // FCM registration token (variable length, ~140-180 chars)
      device_fingerprint: string // Stable device identifier (UUID)
      platform: string          // 'web' | 'android' | 'ios'
      status: 'active' | 'stale' | 'invalid'
      created_at: Timestamp
      last_seen_at: Timestamp   // Updated on every successful delivery
      invalidated_at?: Timestamp // Set when status becomes 'invalid'
    }