Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Use the Founder / Developer tabs in the docs sidebar to filter this page for your role.
Ring Platform uses Firebase in two separate paths — a server-side Admin SDK v14 (for FCM push, Firestore, Auth, Storage, App Check, and Remote Config) and a client-side browser SDK (for FCM push token registration, Auth state, Firestore real-time, and AI Logic). The two paths use different credential strategies and different SDK versions.
This page covers the full Firebase 14 Enterprise stack in firebase-full ring-db mode: pipeline operations, change streams, text/geo search, FCM Admin HTTP v1 bridge, SSOT atomic writes, AI Logic, Firebase Hosting config, and React 19 cache()-native helpers.
firebase-full is a first-class ring-db mode (elevated from prototyping on 2026-06-30). In this mode, all application data lives in Firebase Firestore, push notifications use Firebase Cloud Messaging, file uploads can use Firebase Storage, and the Firebase Hosting service can serve your Ring clone with a single firebase deploy command.
| Capability | What you get |
|---|---|
| No PostgreSQL needed | All data lives in Firestore — no database server to manage, no connection pooling, no SSL certs |
| Unlimited scale | Firebase manages read, write, and concurrency limits — your clone auto-scales |
| Push notifications | FCM web push with React hooks, service worker, and Server Action token upsert (see Push notifications FCM) |
| AI-powered features | Firebase AI Logic for Gemini 2.5/3.x text generation, chat, and embedding — directly from the SDK |
| Single deploy target | firebase deploy --only hosting deploys your Ring clone to Firebase Hosting with TLS, CDN, and rewrite rules baked in |
Use the Founder / Developer tabs in the docs sidebar to filter this page for your role.
Ring Platform uses Firebase in two separate paths — a server-side Admin SDK v14 (for FCM push, Firestore, Auth, Storage, App Check, and Remote Config) and a client-side browser SDK (for FCM push token registration, Auth state, Firestore real-time, and AI Logic). The two paths use different credential strategies and different SDK versions.
This page covers the full Firebase 14 Enterprise stack in firebase-full ring-db mode: pipeline operations, change streams, text/geo search, FCM Admin HTTP v1 bridge, SSOT atomic writes, AI Logic, Firebase Hosting config, and React 19 cache()-native helpers.
firebase-full is a first-class ring-db mode (elevated from prototyping on 2026-06-30). In this mode, all application data lives in Firebase Firestore, push notifications use Firebase Cloud Messaging, file uploads can use Firebase Storage, and the Firebase Hosting service can serve your Ring clone with a single firebase deploy command.
| Capability | What you get |
|---|---|
| No PostgreSQL needed | All data lives in Firestore — no database server to manage, no connection pooling, no SSL certs |
| Unlimited scale | Firebase manages read, write, and concurrency limits — your clone auto-scales |
| Push notifications | FCM web push with React hooks, service worker, and Server Action token upsert (see Push notifications FCM) |
| AI-powered features | Firebase AI Logic for Gemini 2.5/3.x text generation, chat, and embedding — directly from the SDK |
| Single deploy target | firebase deploy --only hosting deploys your Ring clone to Firebase Hosting with TLS, CDN, and rewrite rules baked in |
Use the Founder / Developer tabs in the docs sidebar to filter this page for your role.
Ring Platform uses Firebase in two separate paths — a server-side Admin SDK v14 (for FCM push, Firestore, Auth, Storage, App Check, and Remote Config) and a client-side browser SDK (for FCM push token registration, Auth state, Firestore real-time, and AI Logic). The two paths use different credential strategies and different SDK versions.
This page covers the full Firebase 14 Enterprise stack in firebase-full ring-db mode: pipeline operations, change streams, text/geo search, FCM Admin HTTP v1 bridge, SSOT atomic writes, AI Logic, Firebase Hosting config, and React 19 cache()-native helpers.
firebase-full is a first-class ring-db mode (elevated from prototyping on 2026-06-30). In this mode, all application data lives in Firebase Firestore, push notifications use Firebase Cloud Messaging, file uploads can use Firebase Storage, and the Firebase Hosting service can serve your Ring clone with a single firebase deploy command.
| Capability | What you get |
|---|---|
| No PostgreSQL needed | All data lives in Firestore — no database server to manage, no connection pooling, no SSL certs |
| Unlimited scale | Firebase manages read, write, and concurrency limits — your clone auto-scales |
| Push notifications | FCM web push with React hooks, service worker, and Server Action token upsert (see Push notifications FCM) |
| AI-powered features | Firebase AI Logic for Gemini 2.5/3.x text generation, chat, and embedding — directly from the SDK |
| Single deploy target | firebase deploy --only hosting deploys your Ring clone to Firebase Hosting with TLS, CDN, and rewrite rules baked in |
| No vendor lock for auth | Auth.js v5 handles all authentication — Firebase Auth is not used |
| Local emulator | NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 auto-connects to the Firebase Local Emulator Suite for offline development |
lib/tunnel) is the canonical real-time transport; RTDB exports exist but are unusedlib/firebase.ts; lib/firebase-client.ts is the only client entry pointcache() reduces per-request read operations by 3–7×| File | Purpose |
|---|---|
lib/firebase-client.ts | Browser Firebase app init with 9 React 19 cache()-wrapped lazy getters — getFirebaseClientApp, getFirebaseFirestoreClient, getFirebaseAuthClient, getFirebaseStorageClient, getFirebaseAIClient |
hooks/use-fcm.ts | 'use client' hook wrapping getMessaging + getToken + onMessage with Server Action upsert |
components/providers/fcm-provider.tsx | React context provider for FCM state across the component tree |
public/firebase-messaging-sw.js | Service worker — Firebase compat SDK v12.9.0 loaded from CDN; handles background push events |
Planned: hooks/use-firebase.ts — use-firebase hook family (useFirebaseApp, useFirestore, useFirebaseAuth, useFirestoreDoc, useFirestoreCollection, useFirestoreCache) following the same pattern as use-fcm.ts.
firebase-admin.server.ts)The Admin SDK uses ADC-first initialization with explicit cert() fallback. This means it works out of the box on Cloud Run, Cloud Functions, and GKE without setting AUTH_FIREBASE_CLIENT_EMAIL or AUTH_FIREBASE_PRIVATE_KEY — only AUTH_FIREBASE_PROJECT_ID is needed.
The singleton survives Next.js dev HMR via globalThis.__RING_FIREBASE_ADMIN_APP__:
| Export | Returns | Active in | Build-mock fallback |
|---|---|---|---|
getAdminApp() | App | Always | Yes |
getAdminDb() | Firestore | firebase-full only | mock in other modes |
getAdminAuth() | Auth | firebase-full only | mock in other modes |
getAdminMessaging() | Messaging | All modes (FCM) | Yes — via MockMessaging |
getAdminStorage() | Storage | firebase-full (planned) | Yes — via MockStorage |
getAdminAppCheck() | AppCheck | firebase-full | Yes — via MockAppCheck |
getAdminRemoteConfig() | RemoteConfig | firebase-full | Yes — via MockRemoteConfig |
getAdminRtdb() | Database | firebase-full (legacy) | Yes — via MockDatabase |
Utility exports: initializeAdminApp(), isAdminAppInitialized(), getAdminAppMetrics(), configureAdminDb(settings), _resetAdminAppForTests()
In k8s-postgres-fcm and supabase-fcm modes, getAdminDb() / getAdminAuth() return mock instances from build-mock.server.ts. The mock satisfies TypeScript types but never connects to Firebase. This prevents Firebase initialization when only PostgreSQL is active.
Environment variable cleaning (important):
The SDK aggressively cleans env var inputs — strips surrounding quotes, converts \\n to real newlines, trims whitespace. The env.local.template convention of double-quoting AUTH_FIREBASE_PRIVATE_KEY is intentional:
lib/firebase-client.ts)Provides 9 React 19 cache()-wrapped lazy getters that are SSR-safe and window-guarded:
| Getter | Returns | Lazy import |
|---|---|---|
getFirebaseClientApp() | FirebaseApp | firebase/app |
getFirebaseFirestoreClient() | Firestore | firebase/firestore |
getFirebaseAuthClient() | Auth | firebase/auth |
getFirebaseStorageClient() | Storage (async) | firebase/storage |
getFirebaseAIClient() | AI (async) | firebase/ai |
Emulator support: Set NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 in .env.local to auto-connect Firestore / Auth / Storage to the Firebase Local Emulator Suite (default ports 8080, 9099, 9199). Or call connectFirebaseClientEmulator({ firestorePort?, authPort?, storagePort? }) for finer control.
Backward-compat: The module still exports { app, db, auth } as direct named exports for hooks/use-fcm.ts and other legacy code. New code should prefer the getFirebase*Client() getters for React 19 cache() deduplication and SSR safety.
Utility exports: validateFirebaseConfig() — returns true when all required NEXT_PUBLIC_FIREBASE_* vars are present and non-placeholder; isFcmConfigured() — returns true when config AND VAPID key are valid; isFirebaseClientReady() — SSR-safe runtime check.
FirebaseAdapter.ts)The FirebaseAdapter implements IDatabaseService and is used only when DB_BACKEND_MODE=firebase-full. It uses Application Default Credentials (ADC) by default, falling back to explicit cert() when credentials are present in the backend config:
The adapter now exposes 19 new methods across 6 domains:
1. Native accessors — raw Admin SDK instances
2. Pipeline operations — subquery joins, bulk update/delete via Firebase 14 Enterprise:
3. Change streams — ordered insert/update/delete events (cache invalidation, audit logs):
4. Text search — full-text search via Cloud Firestore Enterprise text indexes:
5. Geospatial search — radius search via Enterprise geo indexes:
Server-side push notifications via HTTP v1 API. Lazy-initializes getAdminMessaging():
All sendFcm* methods use sendEach() (not legacy sendMulticast()) and include automatic reactive cleanup of invalid tokens (messaging/registration-token-not-registered and messaging/invalid-argument errors mark tokens as invalid in the fcm_tokens collection).
Delegates to lib/services/firebase-service-manager.ts atomic helpers — single Firestore transaction, ACID, prevents read-modify-write race:
Useful for /api/admin/firebase-health endpoints and CI smoke tests.
firebase-service-manager.ts)Provides 30+ cached Firebase operations using React 19 cache() for per-request deduplication. SSOT-aligned to the canonical collection shapes:
| Helper | SSOT collection | Purpose |
|---|---|---|
getCachedUserCreditBalance | users/{userId}.credit_balance | Cached fiat USD credit balance |
getCachedCreditTransactions | users/{userId}.credit_transactions[] | Credit transaction history |
getCachedLatestSubscription | subscription_ledger | Latest subscription (any status) |
getCachedActiveSubscription | subscription_ledger | Active subscription (ACTIVE + active) |
getCachedSubscriptionsDue | subscription_ledger | Due-for-payment batch (cron) |
getCachedPaymentTransaction | payment_transactions | By order_reference |
getCachedWalletTransactions | wallet_transactions | Per-user activity feed |
getCachedDeskOrder | desk_orders | Credit ↔ native-token conversion |
getCachedUserOrders | orders | User order history |
All cached functions are only called when getAdminDb() returns real Firestore. During build (SSG), the mock Firestore methods (now including MockMessaging, MockStorage, MockAppCheck, MockRemoteConfig) return safe empty results. The cache() layer deduplicates within each request, reducing redundant Firestore reads by 3–7×.
Server-side Gemini 2.5/3.x text generation and chat via Firebase AI Logic. Requires optional dependency firebase-admin/vertexai + @google-cloud/vertexai. Without them, helpers return a clear not-supported error (no crash).
For the client-side equivalent in the browser, use getFirebaseAIClient() from lib/firebase-client.ts:
firebase-full mode supports Firebase Hosting as a production deployment target alongside k3s, Vercel, and Docker. Module-level helpers generate a canonical firebase.json:
The default rewrites map the canonical Ring routes:
/api/** → Cloud Function / App Hosting backend/auth/** → Auth.js handlers/trpc/** → tRPC (when present)/tunnel/** → Tunnel WebSocket/SFE handler/_actions/** → React 19 Server Actionsfirebase-messaging-sw.js → Service Worker** → SPA fallback (/index.html)Default headers include Cache-Control: immutable for .js/.css, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, and Permissions-Policy.
build-mock.server.ts)During Next.js static generation (NEXT_PHASE=phase-production-build), Firebase Admin calls are intercepted by mock services. Now includes 7 mock classes (up from 3):
| Mock class | Methods | Admin SDK type |
|---|---|---|
MockFirestore | collection, doc, get, set, update, delete, batch, runTransaction | Firestore |
MockAuth | getUser, createUser, updateUser, deleteUser, listUsers, verifyIdToken | Auth |
MockDatabase | ref, push, set, update, remove, once, on, orderByChild, ... | Database |
MockMessaging | send, sendEach, sendAll, sendToTopic, subscribeToTopic, unsubscribeFromTopic | Messaging |
MockStorage | bucket, file, upload, getFiles, delete, getMetadata + defaultBucket | Storage |
MockAppCheck | createToken, verifyToken | AppCheck |
MockRemoteConfig | getTemplate, publishTemplate, rollback, listVersions | RemoteConfig |
This prevents ~22 redundant Firebase Admin initializations during SSG and reduces build time by approximately 31%.
| Aspect | Admin SDK (firebase-admin.server.ts) | Adapter (FirebaseAdapter.ts) |
|---|---|---|
| Credentials | ADC-first — cert() fallback when AUTH_FIREBASE_* present | ADC-first — matches Admin SDK pattern |
| Env vars | Only AUTH_FIREBASE_PROJECT_ID for ADC; all 3 for cert() | Only AUTH_FIREBASE_PROJECT_ID for ADC |
| Active in | All DB_BACKEND_MODE values (FCM always available) | Only firebase-full |
| Mock in postgres-primary | Yes — all services returned as mocks | No — adapter not registered |
| Import path | firebase-admin/app, firebase-admin/firestore, firebase-admin/messaging, etc. | Same (delegates via dynamic import) |
| Version | v14 (named subpath imports) | v14 |
| React 19 native | globalThis HMR-safe singleton + per-service observability | cache()-wrapped lazy getters + async methods for use() + Suspense |
| Enterprise features | Pipeline, change streams, text/geo, AI Logic (via Vertex AI) | All of the above as adapter methods + module-level exports |
import { cert, initializeApp, type App } from 'firebase-admin/app'
// globalThis-backed singleton prevents "already exists" errors on HMR
const globalForAdmin = globalThis as typeof globalThis & {
__RING_FIREBASE_ADMIN_APP__?: App
}
export function getAdminApp(): App {
if (globalForAdmin.__RING_FIREBASE_ADMIN_APP__) {
return globalForAdmin.__RING_FIREBASE_ADMIN_APP__
}
if (getApps().length > 0) {
const app = getApps()[0]
globalForAdmin.__RING_FIREBASE_ADMIN_APP__ = app
return app
}
// Check if we have explicit credentials for cert() fallback
const hasExplicitCreds =
!!process.env.AUTH_FIREBASE_PROJECT_ID &&
!!process.env.AUTH_FIREBASE_CLIENT_EMAIL &&
!!process.env.AUTH_FIREBASE_PRIVATE_KEY
const appOptions = hasExplicitCreds
? { credential: cert({ /* cleaned env vars */ }) }
: {} // ADC auto-detected from environment
const app = initializeApp(appOptions)
globalForAdmin.__RING_FIREBASE_ADMIN_APP__ = app
return app
}
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----\n"
import { getFirebaseClientApp, getFirebaseFirestoreClient } from '@/lib/firebase-client'
// SSR-safe: returns undefined during server render
const app = getFirebaseClientApp()
const db = getFirebaseFirestoreClient()
import { isEmulatorEnabled, connectFirebaseClientEmulator } from '@/lib/firebase-client'
if (isEmulatorEnabled()) {
await connectFirebaseClientEmulator()
}
// Production (Cloud Run, GKE, Cloud Functions):
// ADC auto-detects from the environment — no manual cert() needed.
// Only AUTH_FIREBASE_PROJECT_ID required.
// Local dev / CI:
// Falls back to explicit cert() when credentials.clientEmail and
// credentials.privateKey are present in the backend config.
// Single document
await db.findById('users', userId)
// => { success, data: T | null, error? }
// Query with filters, ordering, pagination
await db.query({
collection: 'entities',
filters: [{ field: 'status', operator: 'eq', value: 'active' }],
orderBy: [{ field: 'createdAt', direction: 'desc' }],
pagination: { limit: 20, offset: 0 },
})
// Mutations
await db.create('users', data)
await db.update('users', id, { name: 'New' })
await db.delete('users', id)
// Transactions
await db.transaction(async (txn) => { ... })
const native = await adapter.getNativeFirestore()
const auth = await adapter.getNativeAuth()
const messaging = await adapter.getNativeMessaging()
const storage = await adapter.getNativeStorage()
const appCheck = await adapter.getNativeAppCheck()
const result = await adapter.runPipeline('orders', [
{ where: { field: 'status', op: '==', value: 'paid' } },
{ aggregate: { sum: { field: 'amount' }, as: 'total' } },
{ limit: 10 },
])
const { data } = await adapter.onCollectionChange('subscription_ledger', (event) => {
if (event.type === 'modified') {
publishToChannel(event.doc.id, 'subscription:update', event.doc)
}
})
const { data } = await adapter.textSearch('entities', 'organic coffee', {
field: 'description',
limit: 20,
})
const { data } = await adapter.geoSearch('entities',
{ latitude: 49.44, longitude: 32.06 },
50, // radius in km
{ field: 'location', limit: 20 },
)
// Single FCM message
await adapter.sendFcmMessage({
token: 'fcm-registration-token',
notification: { title: 'Hello', body: 'World' },
webpush: { fcmOptions: { link: 'https://ring-platform.org/notifications' } },
})
// Multi-token fan-out to a specific user (auto-cleans dead tokens)
await adapter.sendFcmToUser(userId, { title: 'New message', body: 'Text' },
{ route: '/messages/123' },
)
// Topic broadcast
await adapter.sendFcmToTopic('global-announcements', { title: 'Maintenance', body: 'Tonight 2-4 AM' })
// Token validation
const { data } = await adapter.validateFcmToken(token)
if (!data?.valid) { /* remove from DB */ }
// Reactive cleanup
await adapter.cleanupInvalidFcmTokens(['dead-token-1', 'dead-token-2'])
// Atomic credit balance adjust (fiat USD)
await adapter.creditBalanceAdjust(userId, -10, { id: 'ct_xxx', description: 'Membership fee' })
// Atomic subscription status transition (ledger + user doc)
await adapter.subscriptionStatusUpdate(subId, 'cancelled',
{ membership: { tier: 'SUBSCRIBER' } },
{ cancelled_at: Date.now(), auto_renew: false },
)
// Atomic payment transaction status append (status_history prevention)
await adapter.paymentStatusAppend(orderRef, 'paid', { processor_payload: { ... } })
const diagnostics = await adapter.diagnoseConnection()
// => { backendMode, firebaseConfigured, services: { firestore: true, ... }, errors: [] }
getUserCreditTransactions: getCachedCreditTransactions, // legacy → SSOT
getActiveSubscriptions: getCachedSubscriptionsDue, // legacy → SSOT
getUserOrders: getCachedUserOrders, // legacy → SSOT
import { aiGenerateText, aiChatCompletion } from '@/lib/database/adapters/FirebaseAdapter'
// Text generation
const result = await aiGenerateText({
prompt: 'What is the Ring Platform?',
model: 'gemini-2.5-flash',
temperature: 0.7,
maxOutputTokens: 1024,
})
if (result.success) {
console.log(result.data!.text, result.data!.usage)
}
// Multi-turn chat
const chatResult = await aiChatCompletion({
model: 'gemini-2.5-flash',
messages: [
{ role: 'system', content: 'You are a Ring Platform expert.' },
{ role: 'user', content: 'How do I set up firebase-full?' },
],
})
import { getFirebaseAIClient } from '@/lib/firebase-client'
const ai = await getFirebaseAIClient()
if (ai) {
const result = await ai.getGenerativeModel({ model: 'gemini-2.5-flash' }).generateContent('...')
}
import { generateFirebaseJson, getRingHostingRewrites } from '@/lib/database/adapters/FirebaseAdapter'
import { writeFile } from 'fs/promises'
const config = generateFirebaseJson({
public: 'out',
rewrites: getRingHostingRewrites(), // /api/** → /api, /auth/** → /auth, ** → /index.html
})
await writeFile('firebase.json', JSON.stringify(config, null, 2))
// Then: firebase deploy --only hosting
# For ADC (Cloud Run / GKE / Cloud Functions — recommended):
AUTH_FIREBASE_PROJECT_ID=your_firebase_project_id
# For explicit cert() fallback (local dev / CI — all three required):
AUTH_FIREBASE_PROJECT_ID=your_firebase_project_id
AUTH_FIREBASE_CLIENT_EMAIL=your_firebase_client_email
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# Optional:
FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
FIREBASE_PRIVATE_KEY_ID=your_firebase_private_key_id
FIREBASE_CLIENT_ID=your_firebase_client_id
FIREBASE_FIRESTORE_DEBUG=true
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_auth_domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_firebase_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_storage_bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=G-YCZKPV315E
NEXT_PUBLIC_FIREBASE_VAPID_KEY=your_vapid_key
NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 # Enables Firebase Local Emulator Suite auto-connect| No vendor lock for auth | Auth.js v5 handles all authentication — Firebase Auth is not used |
| Local emulator | NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 auto-connects to the Firebase Local Emulator Suite for offline development |
lib/tunnel) is the canonical real-time transport; RTDB exports exist but are unusedlib/firebase.ts; lib/firebase-client.ts is the only client entry pointcache() reduces per-request read operations by 3–7×| File | Purpose |
|---|---|
lib/firebase-client.ts | Browser Firebase app init with 9 React 19 cache()-wrapped lazy getters — getFirebaseClientApp, getFirebaseFirestoreClient, getFirebaseAuthClient, getFirebaseStorageClient, getFirebaseAIClient |
hooks/use-fcm.ts | 'use client' hook wrapping getMessaging + getToken + onMessage with Server Action upsert |
components/providers/fcm-provider.tsx | React context provider for FCM state across the component tree |
public/firebase-messaging-sw.js | Service worker — Firebase compat SDK v12.9.0 loaded from CDN; handles background push events |
Planned: hooks/use-firebase.ts — use-firebase hook family (useFirebaseApp, useFirestore, useFirebaseAuth, useFirestoreDoc, useFirestoreCollection, useFirestoreCache) following the same pattern as use-fcm.ts.
firebase-admin.server.ts)The Admin SDK uses ADC-first initialization with explicit cert() fallback. This means it works out of the box on Cloud Run, Cloud Functions, and GKE without setting AUTH_FIREBASE_CLIENT_EMAIL or AUTH_FIREBASE_PRIVATE_KEY — only AUTH_FIREBASE_PROJECT_ID is needed.
The singleton survives Next.js dev HMR via globalThis.__RING_FIREBASE_ADMIN_APP__:
| Export | Returns | Active in | Build-mock fallback |
|---|---|---|---|
getAdminApp() | App | Always | Yes |
getAdminDb() | Firestore | firebase-full only | mock in other modes |
getAdminAuth() | Auth | firebase-full only | mock in other modes |
getAdminMessaging() | Messaging | All modes (FCM) | Yes — via MockMessaging |
getAdminStorage() | Storage | firebase-full (planned) | Yes — via MockStorage |
getAdminAppCheck() | AppCheck | firebase-full | Yes — via MockAppCheck |
getAdminRemoteConfig() | RemoteConfig | firebase-full | Yes — via MockRemoteConfig |
getAdminRtdb() | Database | firebase-full (legacy) | Yes — via MockDatabase |
Utility exports: initializeAdminApp(), isAdminAppInitialized(), getAdminAppMetrics(), configureAdminDb(settings), _resetAdminAppForTests()
In k8s-postgres-fcm and supabase-fcm modes, getAdminDb() / getAdminAuth() return mock instances from build-mock.server.ts. The mock satisfies TypeScript types but never connects to Firebase. This prevents Firebase initialization when only PostgreSQL is active.
Environment variable cleaning (important):
The SDK aggressively cleans env var inputs — strips surrounding quotes, converts \\n to real newlines, trims whitespace. The env.local.template convention of double-quoting AUTH_FIREBASE_PRIVATE_KEY is intentional:
lib/firebase-client.ts)Provides 9 React 19 cache()-wrapped lazy getters that are SSR-safe and window-guarded:
| Getter | Returns | Lazy import |
|---|---|---|
getFirebaseClientApp() | FirebaseApp | firebase/app |
getFirebaseFirestoreClient() | Firestore | firebase/firestore |
getFirebaseAuthClient() | Auth | firebase/auth |
getFirebaseStorageClient() | Storage (async) | firebase/storage |
getFirebaseAIClient() | AI (async) | firebase/ai |
Emulator support: Set NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 in .env.local to auto-connect Firestore / Auth / Storage to the Firebase Local Emulator Suite (default ports 8080, 9099, 9199). Or call connectFirebaseClientEmulator({ firestorePort?, authPort?, storagePort? }) for finer control.
Backward-compat: The module still exports { app, db, auth } as direct named exports for hooks/use-fcm.ts and other legacy code. New code should prefer the getFirebase*Client() getters for React 19 cache() deduplication and SSR safety.
Utility exports: validateFirebaseConfig() — returns true when all required NEXT_PUBLIC_FIREBASE_* vars are present and non-placeholder; isFcmConfigured() — returns true when config AND VAPID key are valid; isFirebaseClientReady() — SSR-safe runtime check.
FirebaseAdapter.ts)The FirebaseAdapter implements IDatabaseService and is used only when DB_BACKEND_MODE=firebase-full. It uses Application Default Credentials (ADC) by default, falling back to explicit cert() when credentials are present in the backend config:
The adapter now exposes 19 new methods across 6 domains:
1. Native accessors — raw Admin SDK instances
2. Pipeline operations — subquery joins, bulk update/delete via Firebase 14 Enterprise:
3. Change streams — ordered insert/update/delete events (cache invalidation, audit logs):
4. Text search — full-text search via Cloud Firestore Enterprise text indexes:
5. Geospatial search — radius search via Enterprise geo indexes:
Server-side push notifications via HTTP v1 API. Lazy-initializes getAdminMessaging():
All sendFcm* methods use sendEach() (not legacy sendMulticast()) and include automatic reactive cleanup of invalid tokens (messaging/registration-token-not-registered and messaging/invalid-argument errors mark tokens as invalid in the fcm_tokens collection).
Delegates to lib/services/firebase-service-manager.ts atomic helpers — single Firestore transaction, ACID, prevents read-modify-write race:
Useful for /api/admin/firebase-health endpoints and CI smoke tests.
firebase-service-manager.ts)Provides 30+ cached Firebase operations using React 19 cache() for per-request deduplication. SSOT-aligned to the canonical collection shapes:
| Helper | SSOT collection | Purpose |
|---|---|---|
getCachedUserCreditBalance | users/{userId}.credit_balance | Cached fiat USD credit balance |
getCachedCreditTransactions | users/{userId}.credit_transactions[] | Credit transaction history |
getCachedLatestSubscription | subscription_ledger | Latest subscription (any status) |
getCachedActiveSubscription | subscription_ledger | Active subscription (ACTIVE + active) |
getCachedSubscriptionsDue | subscription_ledger | Due-for-payment batch (cron) |
getCachedPaymentTransaction | payment_transactions | By order_reference |
getCachedWalletTransactions | wallet_transactions | Per-user activity feed |
getCachedDeskOrder | desk_orders | Credit ↔ native-token conversion |
getCachedUserOrders | orders | User order history |
All cached functions are only called when getAdminDb() returns real Firestore. During build (SSG), the mock Firestore methods (now including MockMessaging, MockStorage, MockAppCheck, MockRemoteConfig) return safe empty results. The cache() layer deduplicates within each request, reducing redundant Firestore reads by 3–7×.
Server-side Gemini 2.5/3.x text generation and chat via Firebase AI Logic. Requires optional dependency firebase-admin/vertexai + @google-cloud/vertexai. Without them, helpers return a clear not-supported error (no crash).
For the client-side equivalent in the browser, use getFirebaseAIClient() from lib/firebase-client.ts:
firebase-full mode supports Firebase Hosting as a production deployment target alongside k3s, Vercel, and Docker. Module-level helpers generate a canonical firebase.json:
The default rewrites map the canonical Ring routes:
/api/** → Cloud Function / App Hosting backend/auth/** → Auth.js handlers/trpc/** → tRPC (when present)/tunnel/** → Tunnel WebSocket/SFE handler/_actions/** → React 19 Server Actionsfirebase-messaging-sw.js → Service Worker** → SPA fallback (/index.html)Default headers include Cache-Control: immutable for .js/.css, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, and Permissions-Policy.
build-mock.server.ts)During Next.js static generation (NEXT_PHASE=phase-production-build), Firebase Admin calls are intercepted by mock services. Now includes 7 mock classes (up from 3):
| Mock class | Methods | Admin SDK type |
|---|---|---|
MockFirestore | collection, doc, get, set, update, delete, batch, runTransaction | Firestore |
MockAuth | getUser, createUser, updateUser, deleteUser, listUsers, verifyIdToken | Auth |
MockDatabase | ref, push, set, update, remove, once, on, orderByChild, ... | Database |
MockMessaging | send, sendEach, sendAll, sendToTopic, subscribeToTopic, unsubscribeFromTopic | Messaging |
MockStorage | bucket, file, upload, getFiles, delete, getMetadata + defaultBucket | Storage |
MockAppCheck | createToken, verifyToken | AppCheck |
MockRemoteConfig | getTemplate, publishTemplate, rollback, listVersions | RemoteConfig |
This prevents ~22 redundant Firebase Admin initializations during SSG and reduces build time by approximately 31%.
| Aspect | Admin SDK (firebase-admin.server.ts) | Adapter (FirebaseAdapter.ts) |
|---|---|---|
| Credentials | ADC-first — cert() fallback when AUTH_FIREBASE_* present | ADC-first — matches Admin SDK pattern |
| Env vars | Only AUTH_FIREBASE_PROJECT_ID for ADC; all 3 for cert() | Only AUTH_FIREBASE_PROJECT_ID for ADC |
| Active in | All DB_BACKEND_MODE values (FCM always available) | Only firebase-full |
| Mock in postgres-primary | Yes — all services returned as mocks | No — adapter not registered |
| Import path | firebase-admin/app, firebase-admin/firestore, firebase-admin/messaging, etc. | Same (delegates via dynamic import) |
| Version | v14 (named subpath imports) | v14 |
| React 19 native | globalThis HMR-safe singleton + per-service observability | cache()-wrapped lazy getters + async methods for use() + Suspense |
| Enterprise features | Pipeline, change streams, text/geo, AI Logic (via Vertex AI) | All of the above as adapter methods + module-level exports |
import { cert, initializeApp, type App } from 'firebase-admin/app'
// globalThis-backed singleton prevents "already exists" errors on HMR
const globalForAdmin = globalThis as typeof globalThis & {
__RING_FIREBASE_ADMIN_APP__?: App
}
export function getAdminApp(): App {
if (globalForAdmin.__RING_FIREBASE_ADMIN_APP__) {
return globalForAdmin.__RING_FIREBASE_ADMIN_APP__
}
if (getApps().length > 0) {
const app = getApps()[0]
globalForAdmin.__RING_FIREBASE_ADMIN_APP__ = app
return app
}
// Check if we have explicit credentials for cert() fallback
const hasExplicitCreds =
!!process.env.AUTH_FIREBASE_PROJECT_ID &&
!!process.env.AUTH_FIREBASE_CLIENT_EMAIL &&
!!process.env.AUTH_FIREBASE_PRIVATE_KEY
const appOptions = hasExplicitCreds
? { credential: cert({ /* cleaned env vars */ }) }
: {} // ADC auto-detected from environment
const app = initializeApp(appOptions)
globalForAdmin.__RING_FIREBASE_ADMIN_APP__ = app
return app
}
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----\n"
import { getFirebaseClientApp, getFirebaseFirestoreClient } from '@/lib/firebase-client'
// SSR-safe: returns undefined during server render
const app = getFirebaseClientApp()
const db = getFirebaseFirestoreClient()
import { isEmulatorEnabled, connectFirebaseClientEmulator } from '@/lib/firebase-client'
if (isEmulatorEnabled()) {
await connectFirebaseClientEmulator()
}
// Production (Cloud Run, GKE, Cloud Functions):
// ADC auto-detects from the environment — no manual cert() needed.
// Only AUTH_FIREBASE_PROJECT_ID required.
// Local dev / CI:
// Falls back to explicit cert() when credentials.clientEmail and
// credentials.privateKey are present in the backend config.
// Single document
await db.findById('users', userId)
// => { success, data: T | null, error? }
// Query with filters, ordering, pagination
await db.query({
collection: 'entities',
filters: [{ field: 'status', operator: 'eq', value: 'active' }],
orderBy: [{ field: 'createdAt', direction: 'desc' }],
pagination: { limit: 20, offset: 0 },
})
// Mutations
await db.create('users', data)
await db.update('users', id, { name: 'New' })
await db.delete('users', id)
// Transactions
await db.transaction(async (txn) => { ... })
const native = await adapter.getNativeFirestore()
const auth = await adapter.getNativeAuth()
const messaging = await adapter.getNativeMessaging()
const storage = await adapter.getNativeStorage()
const appCheck = await adapter.getNativeAppCheck()
const result = await adapter.runPipeline('orders', [
{ where: { field: 'status', op: '==', value: 'paid' } },
{ aggregate: { sum: { field: 'amount' }, as: 'total' } },
{ limit: 10 },
])
const { data } = await adapter.onCollectionChange('subscription_ledger', (event) => {
if (event.type === 'modified') {
publishToChannel(event.doc.id, 'subscription:update', event.doc)
}
})
const { data } = await adapter.textSearch('entities', 'organic coffee', {
field: 'description',
limit: 20,
})
const { data } = await adapter.geoSearch('entities',
{ latitude: 49.44, longitude: 32.06 },
50, // radius in km
{ field: 'location', limit: 20 },
)
// Single FCM message
await adapter.sendFcmMessage({
token: 'fcm-registration-token',
notification: { title: 'Hello', body: 'World' },
webpush: { fcmOptions: { link: 'https://ring-platform.org/notifications' } },
})
// Multi-token fan-out to a specific user (auto-cleans dead tokens)
await adapter.sendFcmToUser(userId, { title: 'New message', body: 'Text' },
{ route: '/messages/123' },
)
// Topic broadcast
await adapter.sendFcmToTopic('global-announcements', { title: 'Maintenance', body: 'Tonight 2-4 AM' })
// Token validation
const { data } = await adapter.validateFcmToken(token)
if (!data?.valid) { /* remove from DB */ }
// Reactive cleanup
await adapter.cleanupInvalidFcmTokens(['dead-token-1', 'dead-token-2'])
// Atomic credit balance adjust (fiat USD)
await adapter.creditBalanceAdjust(userId, -10, { id: 'ct_xxx', description: 'Membership fee' })
// Atomic subscription status transition (ledger + user doc)
await adapter.subscriptionStatusUpdate(subId, 'cancelled',
{ membership: { tier: 'SUBSCRIBER' } },
{ cancelled_at: Date.now(), auto_renew: false },
)
// Atomic payment transaction status append (status_history prevention)
await adapter.paymentStatusAppend(orderRef, 'paid', { processor_payload: { ... } })
const diagnostics = await adapter.diagnoseConnection()
// => { backendMode, firebaseConfigured, services: { firestore: true, ... }, errors: [] }
getUserCreditTransactions: getCachedCreditTransactions, // legacy → SSOT
getActiveSubscriptions: getCachedSubscriptionsDue, // legacy → SSOT
getUserOrders: getCachedUserOrders, // legacy → SSOT
import { aiGenerateText, aiChatCompletion } from '@/lib/database/adapters/FirebaseAdapter'
// Text generation
const result = await aiGenerateText({
prompt: 'What is the Ring Platform?',
model: 'gemini-2.5-flash',
temperature: 0.7,
maxOutputTokens: 1024,
})
if (result.success) {
console.log(result.data!.text, result.data!.usage)
}
// Multi-turn chat
const chatResult = await aiChatCompletion({
model: 'gemini-2.5-flash',
messages: [
{ role: 'system', content: 'You are a Ring Platform expert.' },
{ role: 'user', content: 'How do I set up firebase-full?' },
],
})
import { getFirebaseAIClient } from '@/lib/firebase-client'
const ai = await getFirebaseAIClient()
if (ai) {
const result = await ai.getGenerativeModel({ model: 'gemini-2.5-flash' }).generateContent('...')
}
import { generateFirebaseJson, getRingHostingRewrites } from '@/lib/database/adapters/FirebaseAdapter'
import { writeFile } from 'fs/promises'
const config = generateFirebaseJson({
public: 'out',
rewrites: getRingHostingRewrites(), // /api/** → /api, /auth/** → /auth, ** → /index.html
})
await writeFile('firebase.json', JSON.stringify(config, null, 2))
// Then: firebase deploy --only hosting
# For ADC (Cloud Run / GKE / Cloud Functions — recommended):
AUTH_FIREBASE_PROJECT_ID=your_firebase_project_id
# For explicit cert() fallback (local dev / CI — all three required):
AUTH_FIREBASE_PROJECT_ID=your_firebase_project_id
AUTH_FIREBASE_CLIENT_EMAIL=your_firebase_client_email
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# Optional:
FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
FIREBASE_PRIVATE_KEY_ID=your_firebase_private_key_id
FIREBASE_CLIENT_ID=your_firebase_client_id
FIREBASE_FIRESTORE_DEBUG=true
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_auth_domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_firebase_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_storage_bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=G-YCZKPV315E
NEXT_PUBLIC_FIREBASE_VAPID_KEY=your_vapid_key
NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 # Enables Firebase Local Emulator Suite auto-connect| No vendor lock for auth | Auth.js v5 handles all authentication — Firebase Auth is not used |
| Local emulator | NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 auto-connects to the Firebase Local Emulator Suite for offline development |
lib/tunnel) is the canonical real-time transport; RTDB exports exist but are unusedlib/firebase.ts; lib/firebase-client.ts is the only client entry pointcache() reduces per-request read operations by 3–7×| File | Purpose |
|---|---|
lib/firebase-client.ts | Browser Firebase app init with 9 React 19 cache()-wrapped lazy getters — getFirebaseClientApp, getFirebaseFirestoreClient, getFirebaseAuthClient, getFirebaseStorageClient, getFirebaseAIClient |
hooks/use-fcm.ts | 'use client' hook wrapping getMessaging + getToken + onMessage with Server Action upsert |
components/providers/fcm-provider.tsx | React context provider for FCM state across the component tree |
public/firebase-messaging-sw.js | Service worker — Firebase compat SDK v12.9.0 loaded from CDN; handles background push events |
Planned: hooks/use-firebase.ts — use-firebase hook family (useFirebaseApp, useFirestore, useFirebaseAuth, useFirestoreDoc, useFirestoreCollection, useFirestoreCache) following the same pattern as use-fcm.ts.
firebase-admin.server.ts)The Admin SDK uses ADC-first initialization with explicit cert() fallback. This means it works out of the box on Cloud Run, Cloud Functions, and GKE without setting AUTH_FIREBASE_CLIENT_EMAIL or AUTH_FIREBASE_PRIVATE_KEY — only AUTH_FIREBASE_PROJECT_ID is needed.
The singleton survives Next.js dev HMR via globalThis.__RING_FIREBASE_ADMIN_APP__:
| Export | Returns | Active in | Build-mock fallback |
|---|---|---|---|
getAdminApp() | App | Always | Yes |
getAdminDb() | Firestore | firebase-full only | mock in other modes |
getAdminAuth() | Auth | firebase-full only | mock in other modes |
getAdminMessaging() | Messaging | All modes (FCM) | Yes — via MockMessaging |
getAdminStorage() | Storage | firebase-full (planned) | Yes — via MockStorage |
getAdminAppCheck() | AppCheck | firebase-full | Yes — via MockAppCheck |
getAdminRemoteConfig() | RemoteConfig | firebase-full | Yes — via MockRemoteConfig |
getAdminRtdb() | Database | firebase-full (legacy) | Yes — via MockDatabase |
Utility exports: initializeAdminApp(), isAdminAppInitialized(), getAdminAppMetrics(), configureAdminDb(settings), _resetAdminAppForTests()
In k8s-postgres-fcm and supabase-fcm modes, getAdminDb() / getAdminAuth() return mock instances from build-mock.server.ts. The mock satisfies TypeScript types but never connects to Firebase. This prevents Firebase initialization when only PostgreSQL is active.
Environment variable cleaning (important):
The SDK aggressively cleans env var inputs — strips surrounding quotes, converts \\n to real newlines, trims whitespace. The env.local.template convention of double-quoting AUTH_FIREBASE_PRIVATE_KEY is intentional:
lib/firebase-client.ts)Provides 9 React 19 cache()-wrapped lazy getters that are SSR-safe and window-guarded:
| Getter | Returns | Lazy import |
|---|---|---|
getFirebaseClientApp() | FirebaseApp | firebase/app |
getFirebaseFirestoreClient() | Firestore | firebase/firestore |
getFirebaseAuthClient() | Auth | firebase/auth |
getFirebaseStorageClient() | Storage (async) | firebase/storage |
getFirebaseAIClient() | AI (async) | firebase/ai |
Emulator support: Set NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 in .env.local to auto-connect Firestore / Auth / Storage to the Firebase Local Emulator Suite (default ports 8080, 9099, 9199). Or call connectFirebaseClientEmulator({ firestorePort?, authPort?, storagePort? }) for finer control.
Backward-compat: The module still exports { app, db, auth } as direct named exports for hooks/use-fcm.ts and other legacy code. New code should prefer the getFirebase*Client() getters for React 19 cache() deduplication and SSR safety.
Utility exports: validateFirebaseConfig() — returns true when all required NEXT_PUBLIC_FIREBASE_* vars are present and non-placeholder; isFcmConfigured() — returns true when config AND VAPID key are valid; isFirebaseClientReady() — SSR-safe runtime check.
FirebaseAdapter.ts)The FirebaseAdapter implements IDatabaseService and is used only when DB_BACKEND_MODE=firebase-full. It uses Application Default Credentials (ADC) by default, falling back to explicit cert() when credentials are present in the backend config:
The adapter now exposes 19 new methods across 6 domains:
1. Native accessors — raw Admin SDK instances
2. Pipeline operations — subquery joins, bulk update/delete via Firebase 14 Enterprise:
3. Change streams — ordered insert/update/delete events (cache invalidation, audit logs):
4. Text search — full-text search via Cloud Firestore Enterprise text indexes:
5. Geospatial search — radius search via Enterprise geo indexes:
Server-side push notifications via HTTP v1 API. Lazy-initializes getAdminMessaging():
All sendFcm* methods use sendEach() (not legacy sendMulticast()) and include automatic reactive cleanup of invalid tokens (messaging/registration-token-not-registered and messaging/invalid-argument errors mark tokens as invalid in the fcm_tokens collection).
Delegates to lib/services/firebase-service-manager.ts atomic helpers — single Firestore transaction, ACID, prevents read-modify-write race:
Useful for /api/admin/firebase-health endpoints and CI smoke tests.
firebase-service-manager.ts)Provides 30+ cached Firebase operations using React 19 cache() for per-request deduplication. SSOT-aligned to the canonical collection shapes:
| Helper | SSOT collection | Purpose |
|---|---|---|
getCachedUserCreditBalance | users/{userId}.credit_balance | Cached fiat USD credit balance |
getCachedCreditTransactions | users/{userId}.credit_transactions[] | Credit transaction history |
getCachedLatestSubscription | subscription_ledger | Latest subscription (any status) |
getCachedActiveSubscription | subscription_ledger | Active subscription (ACTIVE + active) |
getCachedSubscriptionsDue | subscription_ledger | Due-for-payment batch (cron) |
getCachedPaymentTransaction | payment_transactions | By order_reference |
getCachedWalletTransactions | wallet_transactions | Per-user activity feed |
getCachedDeskOrder | desk_orders | Credit ↔ native-token conversion |
getCachedUserOrders | orders | User order history |
All cached functions are only called when getAdminDb() returns real Firestore. During build (SSG), the mock Firestore methods (now including MockMessaging, MockStorage, MockAppCheck, MockRemoteConfig) return safe empty results. The cache() layer deduplicates within each request, reducing redundant Firestore reads by 3–7×.
Server-side Gemini 2.5/3.x text generation and chat via Firebase AI Logic. Requires optional dependency firebase-admin/vertexai + @google-cloud/vertexai. Without them, helpers return a clear not-supported error (no crash).
For the client-side equivalent in the browser, use getFirebaseAIClient() from lib/firebase-client.ts:
firebase-full mode supports Firebase Hosting as a production deployment target alongside k3s, Vercel, and Docker. Module-level helpers generate a canonical firebase.json:
The default rewrites map the canonical Ring routes:
/api/** → Cloud Function / App Hosting backend/auth/** → Auth.js handlers/trpc/** → tRPC (when present)/tunnel/** → Tunnel WebSocket/SFE handler/_actions/** → React 19 Server Actionsfirebase-messaging-sw.js → Service Worker** → SPA fallback (/index.html)Default headers include Cache-Control: immutable for .js/.css, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, and Permissions-Policy.
build-mock.server.ts)During Next.js static generation (NEXT_PHASE=phase-production-build), Firebase Admin calls are intercepted by mock services. Now includes 7 mock classes (up from 3):
| Mock class | Methods | Admin SDK type |
|---|---|---|
MockFirestore | collection, doc, get, set, update, delete, batch, runTransaction | Firestore |
MockAuth | getUser, createUser, updateUser, deleteUser, listUsers, verifyIdToken | Auth |
MockDatabase | ref, push, set, update, remove, once, on, orderByChild, ... | Database |
MockMessaging | send, sendEach, sendAll, sendToTopic, subscribeToTopic, unsubscribeFromTopic | Messaging |
MockStorage | bucket, file, upload, getFiles, delete, getMetadata + defaultBucket | Storage |
MockAppCheck | createToken, verifyToken | AppCheck |
MockRemoteConfig | getTemplate, publishTemplate, rollback, listVersions | RemoteConfig |
This prevents ~22 redundant Firebase Admin initializations during SSG and reduces build time by approximately 31%.
| Aspect | Admin SDK (firebase-admin.server.ts) | Adapter (FirebaseAdapter.ts) |
|---|---|---|
| Credentials | ADC-first — cert() fallback when AUTH_FIREBASE_* present | ADC-first — matches Admin SDK pattern |
| Env vars | Only AUTH_FIREBASE_PROJECT_ID for ADC; all 3 for cert() | Only AUTH_FIREBASE_PROJECT_ID for ADC |
| Active in | All DB_BACKEND_MODE values (FCM always available) | Only firebase-full |
| Mock in postgres-primary | Yes — all services returned as mocks | No — adapter not registered |
| Import path | firebase-admin/app, firebase-admin/firestore, firebase-admin/messaging, etc. | Same (delegates via dynamic import) |
| Version | v14 (named subpath imports) | v14 |
| React 19 native | globalThis HMR-safe singleton + per-service observability | cache()-wrapped lazy getters + async methods for use() + Suspense |
| Enterprise features | Pipeline, change streams, text/geo, AI Logic (via Vertex AI) | All of the above as adapter methods + module-level exports |
import { cert, initializeApp, type App } from 'firebase-admin/app'
// globalThis-backed singleton prevents "already exists" errors on HMR
const globalForAdmin = globalThis as typeof globalThis & {
__RING_FIREBASE_ADMIN_APP__?: App
}
export function getAdminApp(): App {
if (globalForAdmin.__RING_FIREBASE_ADMIN_APP__) {
return globalForAdmin.__RING_FIREBASE_ADMIN_APP__
}
if (getApps().length > 0) {
const app = getApps()[0]
globalForAdmin.__RING_FIREBASE_ADMIN_APP__ = app
return app
}
// Check if we have explicit credentials for cert() fallback
const hasExplicitCreds =
!!process.env.AUTH_FIREBASE_PROJECT_ID &&
!!process.env.AUTH_FIREBASE_CLIENT_EMAIL &&
!!process.env.AUTH_FIREBASE_PRIVATE_KEY
const appOptions = hasExplicitCreds
? { credential: cert({ /* cleaned env vars */ }) }
: {} // ADC auto-detected from environment
const app = initializeApp(appOptions)
globalForAdmin.__RING_FIREBASE_ADMIN_APP__ = app
return app
}
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----\n"
import { getFirebaseClientApp, getFirebaseFirestoreClient } from '@/lib/firebase-client'
// SSR-safe: returns undefined during server render
const app = getFirebaseClientApp()
const db = getFirebaseFirestoreClient()
import { isEmulatorEnabled, connectFirebaseClientEmulator } from '@/lib/firebase-client'
if (isEmulatorEnabled()) {
await connectFirebaseClientEmulator()
}
// Production (Cloud Run, GKE, Cloud Functions):
// ADC auto-detects from the environment — no manual cert() needed.
// Only AUTH_FIREBASE_PROJECT_ID required.
// Local dev / CI:
// Falls back to explicit cert() when credentials.clientEmail and
// credentials.privateKey are present in the backend config.
// Single document
await db.findById('users', userId)
// => { success, data: T | null, error? }
// Query with filters, ordering, pagination
await db.query({
collection: 'entities',
filters: [{ field: 'status', operator: 'eq', value: 'active' }],
orderBy: [{ field: 'createdAt', direction: 'desc' }],
pagination: { limit: 20, offset: 0 },
})
// Mutations
await db.create('users', data)
await db.update('users', id, { name: 'New' })
await db.delete('users', id)
// Transactions
await db.transaction(async (txn) => { ... })
const native = await adapter.getNativeFirestore()
const auth = await adapter.getNativeAuth()
const messaging = await adapter.getNativeMessaging()
const storage = await adapter.getNativeStorage()
const appCheck = await adapter.getNativeAppCheck()
const result = await adapter.runPipeline('orders', [
{ where: { field: 'status', op: '==', value: 'paid' } },
{ aggregate: { sum: { field: 'amount' }, as: 'total' } },
{ limit: 10 },
])
const { data } = await adapter.onCollectionChange('subscription_ledger', (event) => {
if (event.type === 'modified') {
publishToChannel(event.doc.id, 'subscription:update', event.doc)
}
})
const { data } = await adapter.textSearch('entities', 'organic coffee', {
field: 'description',
limit: 20,
})
const { data } = await adapter.geoSearch('entities',
{ latitude: 49.44, longitude: 32.06 },
50, // radius in km
{ field: 'location', limit: 20 },
)
// Single FCM message
await adapter.sendFcmMessage({
token: 'fcm-registration-token',
notification: { title: 'Hello', body: 'World' },
webpush: { fcmOptions: { link: 'https://ring-platform.org/notifications' } },
})
// Multi-token fan-out to a specific user (auto-cleans dead tokens)
await adapter.sendFcmToUser(userId, { title: 'New message', body: 'Text' },
{ route: '/messages/123' },
)
// Topic broadcast
await adapter.sendFcmToTopic('global-announcements', { title: 'Maintenance', body: 'Tonight 2-4 AM' })
// Token validation
const { data } = await adapter.validateFcmToken(token)
if (!data?.valid) { /* remove from DB */ }
// Reactive cleanup
await adapter.cleanupInvalidFcmTokens(['dead-token-1', 'dead-token-2'])
// Atomic credit balance adjust (fiat USD)
await adapter.creditBalanceAdjust(userId, -10, { id: 'ct_xxx', description: 'Membership fee' })
// Atomic subscription status transition (ledger + user doc)
await adapter.subscriptionStatusUpdate(subId, 'cancelled',
{ membership: { tier: 'SUBSCRIBER' } },
{ cancelled_at: Date.now(), auto_renew: false },
)
// Atomic payment transaction status append (status_history prevention)
await adapter.paymentStatusAppend(orderRef, 'paid', { processor_payload: { ... } })
const diagnostics = await adapter.diagnoseConnection()
// => { backendMode, firebaseConfigured, services: { firestore: true, ... }, errors: [] }
getUserCreditTransactions: getCachedCreditTransactions, // legacy → SSOT
getActiveSubscriptions: getCachedSubscriptionsDue, // legacy → SSOT
getUserOrders: getCachedUserOrders, // legacy → SSOT
import { aiGenerateText, aiChatCompletion } from '@/lib/database/adapters/FirebaseAdapter'
// Text generation
const result = await aiGenerateText({
prompt: 'What is the Ring Platform?',
model: 'gemini-2.5-flash',
temperature: 0.7,
maxOutputTokens: 1024,
})
if (result.success) {
console.log(result.data!.text, result.data!.usage)
}
// Multi-turn chat
const chatResult = await aiChatCompletion({
model: 'gemini-2.5-flash',
messages: [
{ role: 'system', content: 'You are a Ring Platform expert.' },
{ role: 'user', content: 'How do I set up firebase-full?' },
],
})
import { getFirebaseAIClient } from '@/lib/firebase-client'
const ai = await getFirebaseAIClient()
if (ai) {
const result = await ai.getGenerativeModel({ model: 'gemini-2.5-flash' }).generateContent('...')
}
import { generateFirebaseJson, getRingHostingRewrites } from '@/lib/database/adapters/FirebaseAdapter'
import { writeFile } from 'fs/promises'
const config = generateFirebaseJson({
public: 'out',
rewrites: getRingHostingRewrites(), // /api/** → /api, /auth/** → /auth, ** → /index.html
})
await writeFile('firebase.json', JSON.stringify(config, null, 2))
// Then: firebase deploy --only hosting
# For ADC (Cloud Run / GKE / Cloud Functions — recommended):
AUTH_FIREBASE_PROJECT_ID=your_firebase_project_id
# For explicit cert() fallback (local dev / CI — all three required):
AUTH_FIREBASE_PROJECT_ID=your_firebase_project_id
AUTH_FIREBASE_CLIENT_EMAIL=your_firebase_client_email
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# Optional:
FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
FIREBASE_PRIVATE_KEY_ID=your_firebase_private_key_id
FIREBASE_CLIENT_ID=your_firebase_client_id
FIREBASE_FIRESTORE_DEBUG=true
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_auth_domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_firebase_project_id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_storage_bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_app_id
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=G-YCZKPV315E
NEXT_PUBLIC_FIREBASE_VAPID_KEY=your_vapid_key
NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1 # Enables Firebase Local Emulator Suite auto-connect