Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
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.
user_id from the authenticated session (never from the client)k8s-postgres-fcm and supabase-fcm modes, FCM works with PostgreSQL onlyUse 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.
user_id from the authenticated session (never from the client)k8s-postgres-fcm and supabase-fcm modes, FCM works with PostgreSQL onlyUse 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.
user_id from the authenticated session (never from the client)k8s-postgres-fcm and supabase-fcm modes, FCM works with PostgreSQL only/firebase-messaging-sw.js.DB_BACKEND_MODE values. No Firestore needed for push alone.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.
React apps should use the Server Action as the primary path. The API route is for non-React clients and is rate-limited.
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.
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.
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().
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.
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 authenticated429 — Rate limit exceeded (see Retry-After header)500 — Server errorThis 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):
In firebase-full mode, the FirebaseAdapter now exposes dedicated FCM Admin bridge methods that use the HTTP v1 API exclusively (sendEach() not legacy sendMulticast()):
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.
On logout or when the user disables push:
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.DELETE /api/notifications/fcm/register with body { "deviceFingerprint": "same-uuid-as-register" } for non-React clients.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.
Split configuration so that only public, safe values are exposed to the client.
VAPID private key and Firebase service account credentials must never be exposed to the client. Keep them server-only.
| Purpose | Variables | Where |
|---|---|---|
| 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_KEY | Client-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 |
| Issue | Cause | What to do |
|---|---|---|
| Permission denied or token not received | User denied notification permission or permission not yet requested | Request permission before calling getToken(); handle permission === 'denied' in the UI |
| 429 on register | Rate limit exceeded | Use the Server Action from React apps; for API clients, respect Retry-After and reduce register frequency |
| Invalid deviceFingerprint | Validation failure | Use a string of length 1–128 with only letters, numbers, hyphens, and underscores |
| 401 on register | Not authenticated | Ensure a valid session (cookie or Bearer) is sent; do not send userId in the body |
| UNREGISTERED on send | Token is stale / invalid | sendFcmToUser auto-handles this via cleanupInvalidFcmTokens — no manual action needed |
| sendMulticast deprecation | Legacy API called | Use sendEach() from the Admin SDK (not sendMulticast() or sendAll()) — this is what sendFcmToUser uses |
| Service worker not receiving | SW not at root scope | Ensure public/firebase-messaging-sw.js is served at /firebase-messaging-sw.js; register with root scope |
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.
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'])
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'
}/firebase-messaging-sw.js.DB_BACKEND_MODE values. No Firestore needed for push alone.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.
React apps should use the Server Action as the primary path. The API route is for non-React clients and is rate-limited.
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.
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.
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().
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.
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 authenticated429 — Rate limit exceeded (see Retry-After header)500 — Server errorThis 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):
In firebase-full mode, the FirebaseAdapter now exposes dedicated FCM Admin bridge methods that use the HTTP v1 API exclusively (sendEach() not legacy sendMulticast()):
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.
On logout or when the user disables push:
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.DELETE /api/notifications/fcm/register with body { "deviceFingerprint": "same-uuid-as-register" } for non-React clients.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.
Split configuration so that only public, safe values are exposed to the client.
VAPID private key and Firebase service account credentials must never be exposed to the client. Keep them server-only.
| Purpose | Variables | Where |
|---|---|---|
| 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_KEY | Client-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 |
| Issue | Cause | What to do |
|---|---|---|
| Permission denied or token not received | User denied notification permission or permission not yet requested | Request permission before calling getToken(); handle permission === 'denied' in the UI |
| 429 on register | Rate limit exceeded | Use the Server Action from React apps; for API clients, respect Retry-After and reduce register frequency |
| Invalid deviceFingerprint | Validation failure | Use a string of length 1–128 with only letters, numbers, hyphens, and underscores |
| 401 on register | Not authenticated | Ensure a valid session (cookie or Bearer) is sent; do not send userId in the body |
| UNREGISTERED on send | Token is stale / invalid | sendFcmToUser auto-handles this via cleanupInvalidFcmTokens — no manual action needed |
| sendMulticast deprecation | Legacy API called | Use sendEach() from the Admin SDK (not sendMulticast() or sendAll()) — this is what sendFcmToUser uses |
| Service worker not receiving | SW not at root scope | Ensure public/firebase-messaging-sw.js is served at /firebase-messaging-sw.js; register with root scope |
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.
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'])
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'
}/firebase-messaging-sw.js.DB_BACKEND_MODE values. No Firestore needed for push alone.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.
React apps should use the Server Action as the primary path. The API route is for non-React clients and is rate-limited.
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.
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.
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().
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.
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 authenticated429 — Rate limit exceeded (see Retry-After header)500 — Server errorThis 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):
In firebase-full mode, the FirebaseAdapter now exposes dedicated FCM Admin bridge methods that use the HTTP v1 API exclusively (sendEach() not legacy sendMulticast()):
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.
On logout or when the user disables push:
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.DELETE /api/notifications/fcm/register with body { "deviceFingerprint": "same-uuid-as-register" } for non-React clients.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.
Split configuration so that only public, safe values are exposed to the client.
VAPID private key and Firebase service account credentials must never be exposed to the client. Keep them server-only.
| Purpose | Variables | Where |
|---|---|---|
| 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_KEY | Client-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 |
| Issue | Cause | What to do |
|---|---|---|
| Permission denied or token not received | User denied notification permission or permission not yet requested | Request permission before calling getToken(); handle permission === 'denied' in the UI |
| 429 on register | Rate limit exceeded | Use the Server Action from React apps; for API clients, respect Retry-After and reduce register frequency |
| Invalid deviceFingerprint | Validation failure | Use a string of length 1–128 with only letters, numbers, hyphens, and underscores |
| 401 on register | Not authenticated | Ensure a valid session (cookie or Bearer) is sent; do not send userId in the body |
| UNREGISTERED on send | Token is stale / invalid | sendFcmToUser auto-handles this via cleanupInvalidFcmTokens — no manual action needed |
| sendMulticast deprecation | Legacy API called | Use sendEach() from the Admin SDK (not sendMulticast() or sendAll()) — this is what sendFcmToUser uses |
| Service worker not receiving | SW not at root scope | Ensure public/firebase-messaging-sw.js is served at /firebase-messaging-sw.js; register with root scope |
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.
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'])
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'
}