Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Preparing Ring Platform content
Preparing Ring Platform content
Preparing Ring Platform content
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.
Shipped (required for browser FCM): 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 variable is passed to getToken({ vapidKey }). Confirmed for ring-platform.
Staged (not consumed by Ring code yet): VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT live in env.local.template, clone secrets, and k8s for the planned web-push dual-stack. FCM send still uses AUTH_FIREBASE_* Admin credentials — not VAPID_PRIVATE_KEY. The web-push package, services/push/, and push-sw.js are not in the tree yet.
| Scenario | What to do |
|---|---|
| New clone / white-label | Create (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 env | Suspect cross-project certificate leak — regenerate Web Push certificates for the clone’s NEXT_PUBLIC_FIREBASE_PROJECT_ID, then rebuild the image (browser bundle is build-time) |
| Staging dual-stack secrets early | You may set to the same Console public key and store / in secrets; Ring code does not read them until ships |
Prerequisite: notification types and channels before deep FCM setup.
Same-workflow: FCM register API contract and env truth for push.
Deep-dive: Firebase Admin, client SDK, and FCM adapter bridge.
Depends-on: Postgres-primary mode where FCM tokens live in SQL.
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.
Shipped (required for browser FCM): 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 variable is passed to getToken({ vapidKey }). Confirmed for ring-platform.
Staged (not consumed by Ring code yet): VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT live in env.local.template, clone secrets, and k8s for the planned web-push dual-stack. FCM send still uses AUTH_FIREBASE_* Admin credentials — not VAPID_PRIVATE_KEY. The web-push package, services/push/, and push-sw.js are not in the tree yet.
| Scenario | What to do |
|---|---|
| New clone / white-label | Create (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 env | Suspect cross-project certificate leak — regenerate Web Push certificates for the clone’s NEXT_PUBLIC_FIREBASE_PROJECT_ID, then rebuild the image (browser bundle is build-time) |
| Staging dual-stack secrets early | You may set to the same Console public key and store / in secrets; Ring code does not read them until ships |
Prerequisite: notification types and channels before deep FCM setup.
Same-workflow: FCM register API contract and env truth for push.
Deep-dive: Firebase Admin, client SDK, and FCM adapter bridge.
Depends-on: Postgres-primary mode where FCM tokens live in SQL.
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.
Shipped (required for browser FCM): 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 variable is passed to getToken({ vapidKey }). Confirmed for ring-platform.
Staged (not consumed by Ring code yet): VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT live in env.local.template, clone secrets, and k8s for the planned web-push dual-stack. FCM send still uses AUTH_FIREBASE_* Admin credentials — not VAPID_PRIVATE_KEY. The web-push package, services/push/, and push-sw.js are not in the tree yet.
| Scenario | What to do |
|---|---|
| New clone / white-label | Create (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 env | Suspect cross-project certificate leak — regenerate Web Push certificates for the clone’s NEXT_PUBLIC_FIREBASE_PROJECT_ID, then rebuild the image (browser bundle is build-time) |
| Staging dual-stack secrets early | You may set to the same Console public key and store / in secrets; Ring code does not read them until ships |
Prerequisite: notification types and channels before deep FCM setup.
Same-workflow: FCM register API contract and env truth for push.
Deep-dive: Firebase Admin, client SDK, and FCM adapter bridge.
Depends-on: Postgres-primary mode where FCM tokens live in SQL.
VAPID_PUBLIC_KEYVAPID_PRIVATE_KEYVAPID_SUBJECTweb-pushNEXT_PUBLIC_FIREBASE_VAPID_KEY (also as Docker build-arg)AUTH_FIREBASE_PROJECT_ID / AUTH_FIREBASE_CLIENT_EMAIL / AUTH_FIREBASE_PRIVATE_KEY/firebase-messaging-sw.jsfeatures/notifications/services/fcm-service.tsFirebaseAdapter.sendFcm*sendEachPlanned webpush-service.ts using web-push |
| npm | firebase, firebase-admin | web-push not installed yet |
| Variable | Role | Status |
|---|---|---|
NEXT_PUBLIC_FIREBASE_VAPID_KEY | Firebase Console Web Push certificate (public) for getToken() | Required — build-arg + client; only FCM subscribe reader |
VAPID_PUBLIC_KEY | Staged public for future web-push | Staged in secrets/env — no process.env.VAPID_* readers yet; may equal the Console public when preparing dual-stack |
VAPID_PRIVATE_KEY | Staged private for future web-push (server-only) | Staged — unused by FCM Admin send |
VAPID_SUBJECT | mailto: or https: contact URI for Web Push | Staged |
FCM server send uses Admin SDK credentials (AUTH_FIREBASE_*), not VAPID_PRIVATE_KEY. Do not invent FCM_SERVER_KEY, FCM_VAPID_KEY, or FIREBASE_WEB_PUSH_* names — those are retired / never were Ring SSOT.
lib/firebase-client.ts detects known ring-main certificate prefixes (BKQ4OAwA…) when NEXT_PUBLIC_FIREBASE_PROJECT_ID is not ring-main. Symptom: token-subscribe-failed / missing authentication credential. Always use the Web Push certificate from the same Firebase project as the clone’s client config.
NEXT_PUBLIC_FIREBASE_VAPID_KEY is baked into the Next.js client bundle at image build. Cluster ConfigMap envFrom can update process env, but browsers keep the previous inlined value until rebuild. Stage VAPID_* in Secret for future dual-stack without waiting on that rebuild.
user_id; never trust a client-supplied user id/firebase-messaging-sw.jsNEXT_PUBLIC_FIREBASE_* + matching NEXT_PUBLIC_FIREBASE_VAPID_KEYAUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY (or ADC where applicable)Stable device fingerprint
Persist a UUID (e.g. localStorage) and reuse it. Upsert key is (user_id, device_fingerprint).
Permission + FCM token
Notification.requestPermission(), then Firebase Messaging getToken() with vapidKey from NEXT_PUBLIC_FIREBASE_VAPID_KEY in a client component (hooks/use-fcm.ts).
Persist on the server
use-fcm prefers POST /api/notifications/fcm/register (avoids Server Action RSC revalidation storms). React can still call upsertFcmToken from app/_actions/fcm.ts when appropriate. Body: token, deviceFingerprint, optional deviceInfo / platform. Auth required.
Unregister on logout
useAuth().signOut() runs device unregister (unregisterFcmToken / fingerprint invalidation). Non-React: DELETE /api/notifications/fcm/register with the same deviceFingerprint.
| Module | Path |
|---|---|
| Domain FCM send + invalid-token cleanup | features/notifications/services/fcm-service.ts |
| firebase-full adapter bridge | lib/database/adapters/FirebaseAdapter.ts — sendFcmMessage, sendFcmToUser, sendFcmToTopic, validateFcmToken, cleanupInvalidFcmTokens |
| Admin messaging accessor | lib/firebase-admin.server.ts → getAdminMessaging() |
| Token DB layer | lib/notifications/fcm-token-db.ts |
| Client hook / SW | hooks/use-fcm.ts, public/firebase-messaging-sw.js |
PostgreSQL schema: data/migrations/016_fcm_jsonb_schema.sql.
| Purpose | Variables | Where |
|---|---|---|
| Firebase client + FCM subscribe | NEXT_PUBLIC_FIREBASE_API_KEY, …_AUTH_DOMAIN, …_PROJECT_ID, …_MESSAGING_SENDER_ID, …_APP_ID, NEXT_PUBLIC_FIREBASE_VAPID_KEY | Client / Docker build-arg |
| Admin send | AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY; DB_BACKEND_MODE | Server-only |
RFC Web Push (web-push) | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT | Staged in secrets / env — no code readers until dual-stack lands |
| Issue | Cause | Fix |
|---|---|---|
| token-subscribe-failed / auth credential | Certificate from another Firebase project (e.g. ring-main BKQ4… on a non-ring-main project) | Regenerate Web Push certificates for this PROJECT_ID; rebuild image |
| ConfigMap updated but browser still old | NEXT_PUBLIC_* inlined at build | Rebuild/redeploy with matching build-arg |
| Permission denied | User denied notifications | Request permission before getToken() |
| 401 on register | No session | Cookie / Bearer required |
| UNREGISTERED on send | Stale token | FCMService / sendFcmToUser marks row invalid |
Expecting web-push dual delivery | Not shipped | Keep FCM path; VAPID_* stay staged only |
See-also: how token storage routes by DB_BACKEND_MODE.
VAPID_PUBLIC_KEYVAPID_PRIVATE_KEYVAPID_SUBJECTweb-pushNEXT_PUBLIC_FIREBASE_VAPID_KEY (also as Docker build-arg)AUTH_FIREBASE_PROJECT_ID / AUTH_FIREBASE_CLIENT_EMAIL / AUTH_FIREBASE_PRIVATE_KEY/firebase-messaging-sw.jsfeatures/notifications/services/fcm-service.tsFirebaseAdapter.sendFcm*sendEachPlanned webpush-service.ts using web-push |
| npm | firebase, firebase-admin | web-push not installed yet |
| Variable | Role | Status |
|---|---|---|
NEXT_PUBLIC_FIREBASE_VAPID_KEY | Firebase Console Web Push certificate (public) for getToken() | Required — build-arg + client; only FCM subscribe reader |
VAPID_PUBLIC_KEY | Staged public for future web-push | Staged in secrets/env — no process.env.VAPID_* readers yet; may equal the Console public when preparing dual-stack |
VAPID_PRIVATE_KEY | Staged private for future web-push (server-only) | Staged — unused by FCM Admin send |
VAPID_SUBJECT | mailto: or https: contact URI for Web Push | Staged |
FCM server send uses Admin SDK credentials (AUTH_FIREBASE_*), not VAPID_PRIVATE_KEY. Do not invent FCM_SERVER_KEY, FCM_VAPID_KEY, or FIREBASE_WEB_PUSH_* names — those are retired / never were Ring SSOT.
lib/firebase-client.ts detects known ring-main certificate prefixes (BKQ4OAwA…) when NEXT_PUBLIC_FIREBASE_PROJECT_ID is not ring-main. Symptom: token-subscribe-failed / missing authentication credential. Always use the Web Push certificate from the same Firebase project as the clone’s client config.
NEXT_PUBLIC_FIREBASE_VAPID_KEY is baked into the Next.js client bundle at image build. Cluster ConfigMap envFrom can update process env, but browsers keep the previous inlined value until rebuild. Stage VAPID_* in Secret for future dual-stack without waiting on that rebuild.
user_id; never trust a client-supplied user id/firebase-messaging-sw.jsNEXT_PUBLIC_FIREBASE_* + matching NEXT_PUBLIC_FIREBASE_VAPID_KEYAUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY (or ADC where applicable)Stable device fingerprint
Persist a UUID (e.g. localStorage) and reuse it. Upsert key is (user_id, device_fingerprint).
Permission + FCM token
Notification.requestPermission(), then Firebase Messaging getToken() with vapidKey from NEXT_PUBLIC_FIREBASE_VAPID_KEY in a client component (hooks/use-fcm.ts).
Persist on the server
use-fcm prefers POST /api/notifications/fcm/register (avoids Server Action RSC revalidation storms). React can still call upsertFcmToken from app/_actions/fcm.ts when appropriate. Body: token, deviceFingerprint, optional deviceInfo / platform. Auth required.
Unregister on logout
useAuth().signOut() runs device unregister (unregisterFcmToken / fingerprint invalidation). Non-React: DELETE /api/notifications/fcm/register with the same deviceFingerprint.
| Module | Path |
|---|---|
| Domain FCM send + invalid-token cleanup | features/notifications/services/fcm-service.ts |
| firebase-full adapter bridge | lib/database/adapters/FirebaseAdapter.ts — sendFcmMessage, sendFcmToUser, sendFcmToTopic, validateFcmToken, cleanupInvalidFcmTokens |
| Admin messaging accessor | lib/firebase-admin.server.ts → getAdminMessaging() |
| Token DB layer | lib/notifications/fcm-token-db.ts |
| Client hook / SW | hooks/use-fcm.ts, public/firebase-messaging-sw.js |
PostgreSQL schema: data/migrations/016_fcm_jsonb_schema.sql.
| Purpose | Variables | Where |
|---|---|---|
| Firebase client + FCM subscribe | NEXT_PUBLIC_FIREBASE_API_KEY, …_AUTH_DOMAIN, …_PROJECT_ID, …_MESSAGING_SENDER_ID, …_APP_ID, NEXT_PUBLIC_FIREBASE_VAPID_KEY | Client / Docker build-arg |
| Admin send | AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY; DB_BACKEND_MODE | Server-only |
RFC Web Push (web-push) | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT | Staged in secrets / env — no code readers until dual-stack lands |
| Issue | Cause | Fix |
|---|---|---|
| token-subscribe-failed / auth credential | Certificate from another Firebase project (e.g. ring-main BKQ4… on a non-ring-main project) | Regenerate Web Push certificates for this PROJECT_ID; rebuild image |
| ConfigMap updated but browser still old | NEXT_PUBLIC_* inlined at build | Rebuild/redeploy with matching build-arg |
| Permission denied | User denied notifications | Request permission before getToken() |
| 401 on register | No session | Cookie / Bearer required |
| UNREGISTERED on send | Stale token | FCMService / sendFcmToUser marks row invalid |
Expecting web-push dual delivery | Not shipped | Keep FCM path; VAPID_* stay staged only |
See-also: how token storage routes by DB_BACKEND_MODE.
VAPID_PUBLIC_KEYVAPID_PRIVATE_KEYVAPID_SUBJECTweb-pushNEXT_PUBLIC_FIREBASE_VAPID_KEY (also as Docker build-arg)AUTH_FIREBASE_PROJECT_ID / AUTH_FIREBASE_CLIENT_EMAIL / AUTH_FIREBASE_PRIVATE_KEY/firebase-messaging-sw.jsfeatures/notifications/services/fcm-service.tsFirebaseAdapter.sendFcm*sendEachPlanned webpush-service.ts using web-push |
| npm | firebase, firebase-admin | web-push not installed yet |
| Variable | Role | Status |
|---|---|---|
NEXT_PUBLIC_FIREBASE_VAPID_KEY | Firebase Console Web Push certificate (public) for getToken() | Required — build-arg + client; only FCM subscribe reader |
VAPID_PUBLIC_KEY | Staged public for future web-push | Staged in secrets/env — no process.env.VAPID_* readers yet; may equal the Console public when preparing dual-stack |
VAPID_PRIVATE_KEY | Staged private for future web-push (server-only) | Staged — unused by FCM Admin send |
VAPID_SUBJECT | mailto: or https: contact URI for Web Push | Staged |
FCM server send uses Admin SDK credentials (AUTH_FIREBASE_*), not VAPID_PRIVATE_KEY. Do not invent FCM_SERVER_KEY, FCM_VAPID_KEY, or FIREBASE_WEB_PUSH_* names — those are retired / never were Ring SSOT.
lib/firebase-client.ts detects known ring-main certificate prefixes (BKQ4OAwA…) when NEXT_PUBLIC_FIREBASE_PROJECT_ID is not ring-main. Symptom: token-subscribe-failed / missing authentication credential. Always use the Web Push certificate from the same Firebase project as the clone’s client config.
NEXT_PUBLIC_FIREBASE_VAPID_KEY is baked into the Next.js client bundle at image build. Cluster ConfigMap envFrom can update process env, but browsers keep the previous inlined value until rebuild. Stage VAPID_* in Secret for future dual-stack without waiting on that rebuild.
user_id; never trust a client-supplied user id/firebase-messaging-sw.jsNEXT_PUBLIC_FIREBASE_* + matching NEXT_PUBLIC_FIREBASE_VAPID_KEYAUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY (or ADC where applicable)Stable device fingerprint
Persist a UUID (e.g. localStorage) and reuse it. Upsert key is (user_id, device_fingerprint).
Permission + FCM token
Notification.requestPermission(), then Firebase Messaging getToken() with vapidKey from NEXT_PUBLIC_FIREBASE_VAPID_KEY in a client component (hooks/use-fcm.ts).
Persist on the server
use-fcm prefers POST /api/notifications/fcm/register (avoids Server Action RSC revalidation storms). React can still call upsertFcmToken from app/_actions/fcm.ts when appropriate. Body: token, deviceFingerprint, optional deviceInfo / platform. Auth required.
Unregister on logout
useAuth().signOut() runs device unregister (unregisterFcmToken / fingerprint invalidation). Non-React: DELETE /api/notifications/fcm/register with the same deviceFingerprint.
| Module | Path |
|---|---|
| Domain FCM send + invalid-token cleanup | features/notifications/services/fcm-service.ts |
| firebase-full adapter bridge | lib/database/adapters/FirebaseAdapter.ts — sendFcmMessage, sendFcmToUser, sendFcmToTopic, validateFcmToken, cleanupInvalidFcmTokens |
| Admin messaging accessor | lib/firebase-admin.server.ts → getAdminMessaging() |
| Token DB layer | lib/notifications/fcm-token-db.ts |
| Client hook / SW | hooks/use-fcm.ts, public/firebase-messaging-sw.js |
PostgreSQL schema: data/migrations/016_fcm_jsonb_schema.sql.
| Purpose | Variables | Where |
|---|---|---|
| Firebase client + FCM subscribe | NEXT_PUBLIC_FIREBASE_API_KEY, …_AUTH_DOMAIN, …_PROJECT_ID, …_MESSAGING_SENDER_ID, …_APP_ID, NEXT_PUBLIC_FIREBASE_VAPID_KEY | Client / Docker build-arg |
| Admin send | AUTH_FIREBASE_PROJECT_ID, AUTH_FIREBASE_CLIENT_EMAIL, AUTH_FIREBASE_PRIVATE_KEY; DB_BACKEND_MODE | Server-only |
RFC Web Push (web-push) | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT | Staged in secrets / env — no code readers until dual-stack lands |
| Issue | Cause | Fix |
|---|---|---|
| token-subscribe-failed / auth credential | Certificate from another Firebase project (e.g. ring-main BKQ4… on a non-ring-main project) | Regenerate Web Push certificates for this PROJECT_ID; rebuild image |
| ConfigMap updated but browser still old | NEXT_PUBLIC_* inlined at build | Rebuild/redeploy with matching build-arg |
| Permission denied | User denied notifications | Request permission before getToken() |
| 401 on register | No session | Cookie / Bearer required |
| UNREGISTERED on send | Stale token | FCMService / sendFcmToUser marks row invalid |
Expecting web-push dual delivery | Not shipped | Keep FCM path; VAPID_* stay staged only |
See-also: how token storage routes by DB_BACKEND_MODE.