---
title: "Firebase Integration"
description: "Firebase Admin SDK v14, FirebaseAdapter, FCM, Hosting rewrites, and ProcessConductor scheduled jobs via Cloud Scheduler (same /api/cron/* contracts as Vercel) for firebase-full mode."
locale: "en"
---
# Firebase Integration

> **Info**
> Use the **Founder** / **Developer** tabs in the docs sidebar to filter this page for your role. `audience` frontmatter controls content filtering within the page. Sidebar article visibility is controlled separately by `lib/docs/audience-curated-docs.ts`.

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, React 19 cache()-native helpers, and **ProcessConductor scheduled jobs** (Cloud Scheduler → the same `/api/cron/*` URLs as Vercel Cron).

  
  
  
  
  

### For founders

## Why firebase-full mode?

**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.

### What this means for your clone

| 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](/docs/features/push-notifications-fcm.md)) |
| **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 |

### Architecture overview

```mermaid
flowchart TB
  subgraph client["Browser"]
    R19["React 19 App\n(Server + Client Components)"]
    SW["Service Worker\nfirebase-messaging-sw.js"]
    useFCM["useFCM() hook\nFCMProvider"]
    useFirebase["useFirebaseApp/useFirestore\n(planned hook family)"]
  end
  subgraph nextjs["Next.js 16 Server"]
    AdminSDK["firebase-admin.server.ts\ngetAdminApp / getAdminDb\n/ getAdminAuth / getAdminMessaging\n/ getAdminStorage / getAdminAppCheck"]
    Adapter["FirebaseAdapter.ts\nIDatabaseService\n+ 19 Enterprise methods"]
    SSOT["firebase-service-manager.ts\n30+ getCachedXxx SSOT helpers\n3 atomic writes\n6 real-time listeners"]
    BuildMock["build-mock.server.ts\n(SSG + k8s-postgres-fcm)"]
  end
  subgraph firebase["Firebase Project"]
    FS[(Firestore\nsubscription_ledger\npayment_transactions\nwallet_transactions\ndesk_orders\norders\nfcm_tokens)]
    FCM["FCM HTTP v1\n(getAdminMessaging)"]
    AI["AI Logic\nGemini 2.5/3.x\n(firebase/ai / vertexai)"]
    Hosting["Firebase Hosting\n(firebase.json)"]
    AppCheck["App Check"]
    RemoteConfig["Remote Config"]
  end
  R19 -->|server| AdminSDK
  R19 -->|client| useFCM
  R19 -->|server| SSOT
  SSOT --> Adapter
  Adapter --> FS
  AdminSDK --> FS
  AdminSDK --> FCM
  AdminSDK --> AI
  AdminSDK --> AppCheck
  AdminSDK --> RemoteConfig
  useFCM -->|server action| AdminSDK
  SW -->|push event| client
```

### What Firebase features are NOT used

- **Firebase Authentication** — Auth.js v5 handles all auth (Google, Apple, email, crypto wallet)
- **Firebase Realtime Database** — Tunnel (`lib/tunnel`) is the canonical real-time transport; RTDB exports exist but are unused
- **FirebaseUI / client auth SDK** — there is no `lib/firebase.ts`; `lib/firebase-client.ts` is the only client entry point

### Cost profile

- **Firestore** — pay-per-read/write/delete. React 19 `cache()` reduces per-request read operations by 3–7×
- **FCM** — free at any scale
- **Firebase App Check** — included in Spark/Blaze plans
- **Firebase Hosting** — free tier includes 10 GB storage, 100 GB/month transfer
- **AI Logic (Gemini)** — usage-based via Vertex AI pricing; optional to enable
- **No PostgreSQL costs** — no database server to provision or maintain

### Scheduled jobs — firebase.json is not a cron file

Ring’s background health (email CRM poll, reservation cleanup, subscription expiry, settlements, and more) runs through **ProcessConductor** — currently **19** pipelines in `lib/processes/registry.ts`. Each pipeline exposes `GET/POST /api/cron/` gated by `Authorization: Bearer $CRON_SECRET`.

**Critical for Firebase-hosted clones:** `firebase.json` holds Firestore rules/indexes (and Hosting rewrites when generated). It is **not** a scheduler catalog. There is no Firebase-native cron entries file in the Ring tree.

| What you need | Where it lives |
|---------------|----------------|
| Human-readable schedule catalog (UTC) | `vercel.json` → `crons[]` — copy every path/schedule |
| HTTP contracts (same on all backends) | `/api/cron/*` + `CRON_SECRET` |
| Run history labels | Admin → **Background Processes** (`locales/*/modules/admin.json` → `processes.pipelines.*`) |
| Trigger on Firebase Hosting / Cloud Run | **Google Cloud Scheduler** (or any external cron) hitting those URLs |

> **Warning**
> Deploying Hosting alone does **not** schedule ProcessConductor jobs. Wire Cloud Scheduler to the same cron URLs using the [Vercel schedule catalog](/docs/deployment/vercel.md), and set `CRON_SECRET` in the app env.

#### Founder checklist

1. Set a long random `CRON_SECRET` in the Firebase-hosted app environment.
2. Open `vercel.json` in the Ring root — treat `crons[]` as the schedule SSOT (must list every ProcessConductor `cronPath`).
3. Create one Cloud Scheduler job per entry (UTC schedule → `GET` or `POST` your public `/api/cron/...` URL with `Authorization: Bearer `).
4. Confirm runs appear under Admin → Background Processes.
5. Contrast: k3s uses per-job YAML under `k8s/` (e.g. `cronjob-email-processor.yaml`) curling an in-cluster URL — not a single inventory file.

### For developers

## Server-side files

| File | Purpose | Credential strategy |
|------|---------|-------------------|
| `lib/firebase-admin.server.ts` | Firebase Admin SDK v14 singleton — FCM, Auth, Firestore, Storage, App Check, Remote Config, Vertex AI | **ADC-first** — Application Default Credentials on Cloud Run / GKE / Cloud Functions; falls back to explicit `cert()` from `AUTH_FIREBASE_*` env vars for local dev / CI |
| `lib/database/adapters/FirebaseAdapter.ts` | Firestore IDatabaseService implementation — full CRUD, query, batch, transaction, plus **19 Enterprise methods** | **ADC-first** — `cert()` fallback when credentials present in backend config (matches firebase-admin.server.ts) |
| `lib/services/firebase-service-manager.ts` | React 19 `cache()`-deduplicated SSOT read helpers + atomic write primitives for `credit_balance`, `subscription_ledger`, `payment_transactions`, `wallet_transactions`, `desk_orders`, `orders` | Delegates to `getAdminDb()` |
| `lib/firebase/build-mock.server.ts` | Mock services during Next.js SSG build + k8s-postgres-fcm fallback — prevents unnecessary Firebase connections | Returns mocks without connecting |

## Client-side files

| 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 SDK (`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__`:

```typescript
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
}
```

### Exports

| 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:

```bash
AUTH_FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----\n"
```

## Client-side Firebase (`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` |

```typescript
import { getFirebaseClientApp, getFirebaseFirestoreClient } from '@/lib/firebase-client'

// SSR-safe: returns undefined during server render
const app = getFirebaseClientApp()
const db = getFirebaseFirestoreClient()
```

**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.

```typescript
import { isEmulatorEnabled, connectFirebaseClientEmulator } from '@/lib/firebase-client'

if (isEmulatorEnabled()) {
  await connectFirebaseClientEmulator()
}
```

**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 (`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:

```typescript
// 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.
```

### IDatabaseService contract

```typescript
// 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) => { ... })
```

### Firebase 14 Enterprise features (new)

The adapter now exposes **19 new methods** across 6 domains:

**1. Native accessors — raw Admin SDK instances**

```typescript
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()
```

**2. Pipeline operations** — subquery joins, bulk update/delete via Firebase 14 Enterprise:

```typescript
const result = await adapter.runPipeline('orders', [
  { where: { field: 'status', op: '==', value: 'paid' } },
  { aggregate: { sum: { field: 'amount' }, as: 'total' } },
  { limit: 10 },
])
```

**3. Change streams** — ordered insert/update/delete events (cache invalidation, audit logs):

```typescript
const { data } = await adapter.onCollectionChange('subscription_ledger', (event) => {
  if (event.type === 'modified') {
    publishToChannel(event.doc.id, 'subscription:update', event.doc)
  }
})
```

**4. Text search** — full-text search via Cloud Firestore Enterprise text indexes:

```typescript
const { data } = await adapter.textSearch('entities', 'organic coffee', {
  field: 'description',
  limit: 20,
})
```

**5. Geospatial search** — radius search via Enterprise geo indexes:

```typescript
const { data } = await adapter.geoSearch('entities',
  { latitude: 49.44, longitude: 32.06 },
  50, // radius in km
  { field: 'location', limit: 20 },
)
```

### FCM Admin bridge (new)

Server-side push notifications via HTTP v1 API. Lazy-initializes `getAdminMessaging()`:

```typescript
// 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'])
```

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).

### SSOT atomic writes bridge (new)

Delegates to `lib/services/firebase-service-manager.ts` atomic helpers — single Firestore transaction, ACID, prevents read-modify-write race:

```typescript
// 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: { ... } })
```

### Connection diagnostics

```typescript
const diagnostics = await adapter.diagnoseConnection()
// => { backendMode, firebaseConfigured, services: { firestore: true, ... }, errors: [] }
```

Useful for `/api/admin/firebase-health` endpoints and CI smoke tests.

## firebase-service-manager (`firebase-service-manager.ts`)

Provides **30+ cached Firebase operations** using React 19 `cache()` for per-request deduplication. SSOT-aligned to the canonical collection shapes:

### Domain-specific SSOT helpers

| 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 |

### Legacy aliases preserved

```typescript
getUserCreditTransactions: getCachedCreditTransactions, // legacy → SSOT
getActiveSubscriptions: getCachedSubscriptionsDue,       // legacy → SSOT
getUserOrders: getCachedUserOrders,                      // legacy → SSOT
```

### Build-time optimization

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×.

## AI Logic bridge (new)

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).

```typescript
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?' },
  ],
})
```

For the **client-side** equivalent in the browser, use `getFirebaseAIClient()` from `lib/firebase-client.ts`:

```typescript
import { getFirebaseAIClient } from '@/lib/firebase-client'

const ai = await getFirebaseAIClient()
if (ai) {
  const result = await ai.getGenerativeModel({ model: 'gemini-2.5-flash' }).generateContent('...')
}
```

## Firebase Hosting bridge (new)

firebase-full mode supports **Firebase Hosting** as a production deployment target alongside k3s, Vercel, and Docker. Module-level helpers generate a canonical `firebase.json` for **hosting rewrites/headers only** — they do **not** register schedulers or ProcessConductor cron entries:

```typescript
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
```

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 Actions
- `firebase-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`.

> **Tip**
> `generateFirebaseJson` / `getRingHostingRewrites` in `FirebaseAdapter.ts` = Hosting rewrites/headers only. Schedule ProcessConductor jobs with Cloud Scheduler against `/api/cron/*` — see below.

## ProcessConductor cron (Cloud Scheduler)

Same HTTP contracts as Vercel and k3s — different trigger.

| Layer | Path / artifact |
|-------|-----------------|
| Registry SSOT | `lib/processes/registry.ts` — `PIPELINE_IDS` + `PIPELINE_REGISTRY` (**19** pipelines) |
| Routes | `app/api/cron//route.ts` — `GET`/`POST`, fail-closed on `Authorization: Bearer $CRON_SECRET` |
| Schedule catalog | `vercel.json` → `crons[]` (UTC) — **must list every** `cronPath` |
| Ledger | `ProcessConductor.recordRun` |
| Admin labels | `locales/*/modules/admin.json` → `processes.pipelines.*` |
| Human ops doc | `AI-RING/scripts/PIPELINES.md` § ProcessConductor cron pipelines |

### Wire Google Cloud Scheduler

Set `CRON_SECRET` in the Firebase-hosted / Cloud Run app environment (same secret you will send from Scheduler).

Copy every `{ path, schedule }` from root `vercel.json` `crons[]`. Full catalog (UTC) is also documented on [Vercel deployment](/docs/deployment/vercel.md).

For each entry, create a Cloud Scheduler job:

- **Frequency:** the `schedule` string (UTC)
- **Target:** `https://` (e.g. `/api/cron/email-processor`)
- **HTTP method:** `GET` or `POST` (both accepted by Ring cron routes)
- **Header:** `Authorization: Bearer `

Smoke-test one job:

```bash
curl -sS -H "Authorization: Bearer $CRON_SECRET" \
  "https:///api/cron/cleanup-usernames"
```

Expect `200` when authorized; `401` when the secret is missing/wrong (fail-closed).

Confirm Admin → Background Processes shows recorded runs. When adding a pipeline: update `registry.ts` + route + locale labels + **append `vercel.json`** (schedule catalog) + optional `k8s/cronjob-*.yaml`.

> **Info**
> Ops notes: `forgejo-robot-gc` supports `?dryRun=1` (classifies without deleting). `forgejo-token-rotate` reads `FORGEJO_TOKEN_ROTATE_MAX_AGE_DAYS` / `FORGEJO_TOKEN_ROTATE_LIMIT`. k3s contrast: per-job YAML under `k8s/` curling an in-cluster URL — not a single inventory file.

## Build-time mocking (`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%.

## Environment variables

### Admin SDK (server-side)

```bash
# 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
```

### Client SDK (browser-side)

```bash
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
# ↑ Cloud Messaging → Web Push certificates (same project). Rebuild after rotate.
NEXT_PUBLIC_FIREBASE_USE_EMULATOR=1  # Enables Firebase Local Emulator Suite auto-connect
```

`isFcmConfigured()` / `validateFcmVapidKey()` / `getFcmVapidKey()` in `lib/firebase-client.ts` require a non-placeholder `NEXT_PUBLIC_FIREBASE_VAPID_KEY`. Mismatched certificates fail at Firebase `getToken` — Ring does not classify key prefixes. Deep push map: [Push notifications (FCM)](/docs/features/push-notifications-fcm.md).

## Key differences between the two Firebase paths

| 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 |

## Related documentation

  
- [deployment/vercel](/docs/deployment/vercel.md) — Same-workflow: vercel.json is the ProcessConductor schedule catalog — copy crons[] into Cloud Scheduler.

  
- [architecture/backend-modes-and-databases](/docs/architecture/backend-modes-and-databases.md) — Prerequisite: firebase-full vs k8s-postgres-fcm vs supabase-fcm.

  
- [features/push-notifications-fcm](/docs/features/push-notifications-fcm.md) — Next-step: FCM + RFC dual-stack register/send, iOS Home Screen, empty RFC-on-Chrome no-op.

  
- [deployment/environment](/docs/deployment/environment.md) — Depends-on: AUTH_FIREBASE_*, NEXT_PUBLIC_FIREBASE_*, and CRON_SECRET.

  
- [architecture/authentication](/docs/architecture/authentication.md) — See-also: Auth.js v5 — Firebase Auth is not used.

  
- [backend/k8s-postgres-fcm](/docs/backend/k8s-postgres-fcm.md) — See-also: PostgreSQL-primary mode with FCM-only Firebase.

  
- [features/admin](/docs/features/admin.md) — Next-step: Admin → Background Processes labels for pipeline runs.

  
- [features/email-ai-crm](/docs/features/email-ai-crm.md) — Same-workflow: email-processor / email-analytics cron pipelines.
