{"slug": "firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend", "title": "Firebase AI Logic in Angular: Client-Side Gemini Without a Custom Backend", "summary": "A developer published a guide showing how to call Google's Gemini models directly from an Angular browser app using Firebase AI Logic, the successor to Vertex AI in Firebase, without building a custom backend proxy. The approach relies on the modular Firebase JS SDK with Angular dependency injection, App Check attestation via reCAPTCHA Enterprise, and a Gemini Developer API backend, requiring only two new Firebase-specific files. The guide walks through Console setup, API enablement, and the first generateContent() call, with a ByteWise demo using function calling for inventory and cart operations.", "body_md": "Since I last wrote [Building AI-powered e-commerce applications using Angular & Firebase AI Logic (formerly Vertex AI in Firebase)](https://dev.to/wayne_gakuo/building-ai-powered-e-commerce-applications-using-angular-firebase-ai-logic-formerly-vertex-ai-4mdi) in 2025, so much has changed that this article may look like a complete rewrite.\n\n**Firebase AI Logic** is the successor to **Vertex AI in Firebase** (May 2025). This post covers the Angular setup only. For the full rename, new APIs, and migration from `firebase/vertexai`, see [Vertex AI in Firebase is now Firebase AI Logic — What Actually Changed](https://dev.to/wayne_gakuo/vertex-ai-in-firebase-is-now-firebase-ai-logic-what-actually-changed-fbb).\n\nOfficial Firebase docs cover each piece, but the order matters. This guide follows the sequence that actually works. We use the modular **`firebase` JS SDK + Angular DI**, so this works on Angular 18+.\n\nMost Gemini tutorials show either **AI Studio + a raw API key** or a **custom Node/Python proxy** you host and secure yourself. Firebase AI Logic sits in between: Gemini runs from your **Angular app in the browser**, but requests go through Firebase's managed path and not a DIY backend you maintain.\n\n**What you get on web:**\n\n| Benefit | Why it matters | \n|---|---|\n| **No custom AI backend** | Call `generateContent()` from a service; skip building Express/Cloud Functions just to hide an API key | \n| **App Check attestation** | Proves traffic comes from your real app (reCAPTCHA Enterprise in prod, debug tokens on localhost) — required for production AI Logic on web | \n| **One Firebase project** | Same Console for Hosting, Auth, Firestore, Remote Config, and AI monitoring | \n| **Gemini Developer API path** | `GoogleAIBackend()` : fast onboarding, Spark plan for prototypes, Console wizard provisions APIs for you | \n| **Upgrade path** | Switch to `AgentPlatformBackend()` later if you need enterprise Vertex/Agent Platform features; often a one-line backend change | \n\n**Good fit for web apps:**\n\n**When to use something else:**\n\nFor ByteWise, the agent reads inventory and updates the cart via **function calling**; still client-side AI Logic, with business logic in Angular services. This guide stops at your first `generateContent()`; the [ByteWise repo](https://github.com/waynegakuo/bytewise) goes further.\n\n```\nFirebase Console (AI Logic + App Check + API keys)\n        ↓\nfirebase.config.ts          ← credentials\nfirebase-ai.ts              ← initializeApp + App Check + getAI (providers)\napp.config.ts               ← register providers once\nai.service.ts               ← inject FIREBASE_AI → generateContent()\n```\n\nYou only **create two new Firebase-specific files** beyond a standard Angular app:\n\n| File | Purpose | \n|---|---|\n| `src/environments/firebase.config.ts` | `firebaseConfig` + reCAPTCHA site key | \n| `src/app/firebase/firebase-ai.ts` | Bootstrap: tokens, App Check, `provideFirebaseAI()` | \n\nEverything else is small edits to files you already have (`app.config.ts`, `index.html`, one service).\n\n`ApplicationConfig` style)`firebase` >= 12.19.0\n\n```\nnpm install firebase@^12.19.0\n```\n\n`</>`)` firebaseConfig` object — you will paste it in Part 2\nWait **5–10 minutes**, then in [Google Cloud console](https://console.cloud.google.com/) (same project ID as Firebase): **APIs & Services** → **Enabled APIs & services** → scroll past the charts to the API table → use **Filter** to confirm **Firebase AI Logic API** and **Gemini API** are listed. Firebase's wizard labels the second one **Gemini Developer API**; GCP now shows **Gemini API** (`generativelanguage.googleapis.com`). You may also see **Gemini for Google Cloud API** when filtering `Gemini` — that is for Gemini inside the Cloud Console and is not required for Firebase AI Logic in your Angular app.\n\nSkipping this step is the most common cause of **403 \"The caller does not have permission\"** later.\n\n**3a. Create a reCAPTCHA Enterprise key**\n\n`localhost`. For example:\n`<project-id>.web.app``<project-id>.firebaseapp.com`` 6L...`\n**3b. Register App Check**\n\n`Basic - Enforced`). However, you can set up additional checks if you want such as `firebaseConfig.apiKey`\nYou can generate your Angular environment files using the command:\n\n```\nng generate environments\n```\n\n`src/environments/firebase.config.ts`\nInside the `environment` folder, create a file with the name `firebase.config.ts` and copy the code snippet below. Replace the `YOUR_` values with the actual values from your Firebase project **Settings** (scroll all the way down to the **SDK setup and configuration** section).\n\nThe `recaptchaEnterpriseSiteKey` is the same as the one in **Part 1: Step 3a**: [Google Cloud](https://console.cloud.google.com/) (same project) → Security → Fraud Defense → Keys → Copy the already created key.\n\n``` python\nimport type { FirebaseOptions } from 'firebase/app';\n\nexport const firebaseConfig: FirebaseOptions = {\n  apiKey: 'YOUR_API_KEY',\n  authDomain: 'YOUR_PROJECT.firebaseapp.com',\n  projectId: 'YOUR_PROJECT_ID',\n  storageBucket: 'YOUR_PROJECT.firebasestorage.app',\n  messagingSenderId: 'YOUR_SENDER_ID',\n  appId: 'YOUR_APP_ID',\n};\n\nexport const recaptchaEnterpriseSiteKey = 'YOUR_RECAPTCHA_SITE_KEY';\n```\n\n`src/environments/environment.model.ts`\nCreate `environment.model.ts` — one shared type for production and development. `appCheckDebugToken` is **optional** so production omits it, while `firebase-ai.ts` can still reference `environment.appCheckDebugToken` without IDE `TS2339` errors (your editor type-checks `environment.ts`, not the dev file Angular swaps in at build time).\n\n``` python\nimport type { FirebaseOptions } from 'firebase/app';\n\nexport interface AppEnvironment {\n  production: boolean;\n  firebaseConfig: FirebaseOptions;\n  recaptchaEnterpriseSiteKey: string;\n  /** Dev/CI only — set in `environment.development.ts`, omit in `environment.ts` */\n  appCheckDebugToken?: boolean | string;\n}\n```\n\n`environment.ts` and `environment.development.ts`\n**`src/environments/environment.ts`** (production — used by `ng build`):\n\n``` js\nimport { firebaseConfig, recaptchaEnterpriseSiteKey } from './firebase.config';\nimport type { AppEnvironment } from './environment.model';\n\nexport const environment: AppEnvironment = {\n  production: true,\n  firebaseConfig,\n  recaptchaEnterpriseSiteKey,\n};\n```\n\n**`src/environments/environment.development.ts`** (local dev — swapped in by `ng serve`):\n\n``` js\nimport { firebaseConfig, recaptchaEnterpriseSiteKey } from './firebase.config';\nimport type { AppEnvironment } from './environment.model';\n\nexport const environment: AppEnvironment = {\n  production: false,\n  firebaseConfig,\n  recaptchaEnterpriseSiteKey,\n  appCheckDebugToken: true,\n};\n```\n\nConfirm `angular.json` has `fileReplacements` under the **development** build configuration ( `ng generate environments` usually adds this):\n\n```\n\"fileReplacements\": [\n  {\n    \"replace\": \"src/environments/environment.ts\",\n    \"with\": \"src/environments/environment.development.ts\"\n  }\n]\n```\n\nAlways import the alias path in app code — **never** `environment.development` directly:\n\n``` js\nimport { environment } from '../../environments/environment';\n```\n\n**NOTE:** On **`ng serve`**, the CLI substitutes `environment.development.ts` wherever you import `environment.ts`. Production builds use the real `environment.ts` with no debug token.\n\n`src/app/firebase/firebase-ai.ts`\nCreate a file with the name `firebase-ai.ts` under a folder `firebase`.\n\n``` js\nimport {\n  inject,\n  InjectionToken,\n  makeEnvironmentProviders,\n  PLATFORM_ID,\n  provideAppInitializer,\n  type EnvironmentProviders,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { getAI, GoogleAIBackend, type AI } from 'firebase/ai';\nimport { initializeApp, type FirebaseApp } from 'firebase/app';\nimport {\n  CustomProvider,\n  initializeAppCheck,\n  ReCaptchaEnterpriseProvider,\n  type AppCheck,\n} from 'firebase/app-check';\nimport { environment } from '../../environments/environment';\n\nexport const FIREBASE_AI = new InjectionToken<AI>('FIREBASE_AI');\nconst FIREBASE_APP_CHECK = new InjectionToken<AppCheck>('FIREBASE_APP_CHECK');\n\nconst firebaseApp = initializeApp(environment.firebaseConfig);\n\nfunction initAppCheck(firebaseApp: FirebaseApp, platformId: object): AppCheck {\n  const isBrowser = isPlatformBrowser(platformId);\n\n  if (isBrowser && !environment.production) {\n    (globalThis as typeof globalThis & { FIREBASE_APPCHECK_DEBUG_TOKEN?: boolean | string })\n      .FIREBASE_APPCHECK_DEBUG_TOKEN ??= environment.appCheckDebugToken ?? true;\n  }\n\n  if (!isBrowser) {\n    return initializeAppCheck(firebaseApp, {\n      provider: new CustomProvider({\n        getToken: async () => ({\n          token: 'ssr-placeholder',\n          expireTimeMillis: Date.now() + 60 * 60 * 1000,\n        }),\n      }),\n      isTokenAutoRefreshEnabled: false,\n    });\n  }\n\n  return initializeAppCheck(firebaseApp, {\n    provider: new ReCaptchaEnterpriseProvider(environment.recaptchaEnterpriseSiteKey),\n    isTokenAutoRefreshEnabled: true,\n  });\n}\n\nexport function provideFirebaseAI(): EnvironmentProviders {\n  return makeEnvironmentProviders([\n    {\n      provide: FIREBASE_APP_CHECK,\n      useFactory: (platformId: object) => initAppCheck(firebaseApp, platformId),\n      deps: [PLATFORM_ID],\n    },\n    {\n      provide: FIREBASE_AI,\n      useFactory: (_appCheck: AppCheck): AI =>\n        getAI(firebaseApp, {\n          backend: new GoogleAIBackend(),\n          useLimitedUseAppCheckTokens: environment.production,\n        }),\n      deps: [FIREBASE_APP_CHECK],\n    },\n    // Eager-init App Check on startup — prints the debug token in DevTools\n    // immediately so you can register it in Firebase Console before Part 3.\n    provideAppInitializer(() => {\n      inject(FIREBASE_APP_CHECK);\n    }),\n  ]);\n}\n```\n\n`src/app/app.config.ts`\n\n``` js\nimport { ApplicationConfig } from '@angular/core';\nimport { provideRouter } from '@angular/router';\nimport { routes } from './app.routes';\nimport { provideFirebaseAI } from './firebase/firebase-ai';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideRouter(routes),\n    provideFirebaseAI(),\n  ],\n};\n```\n\n`src/index.html`\nAdd this inside the `<body>` element, right before `<app-root>`.\n\n```\n<script>\n  if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {\n    self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;\n  }\n</script>\nng serve\n```\n\nOpen **[http://localhost:4200](http://localhost:4200)** (the URL the Angular CLI prints in the terminal). \n\n**Where the token appears:** the Firebase SDK logs it when `initializeAppCheck()` runs with the debug provider active. Open DevTools → **Console**:\n\nSearch/filter for **`AppCheck debug token`** or `debug token`\n\nYou should see a line similar to:\n\n```\nFirebase App Check debug token: \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n```\n\nCopy the UUID (without quotes), then register it:\n\nFirebase Console → **App Check** → Apps → your web app → **⋮** menu → **Manage debug tokens** → **Add debug token** → Give the debug token any name of your choice → paste → save → hard-refresh the app (`Ctrl+Shift+R`).\n\n**NOTE:** The debug token is **not** in `environment.ts`. It's **minted by the SDK** and printed in the Console once App Check initializes on the client. The debug token is ONLY for use during development. Production must use reCAPTCHA, not debug tokens.\n\n`generateContent`\n`src/app/services/ai.service.ts`\n\n``` js\nimport { inject, Injectable } from '@angular/core';\nimport { getGenerativeModel } from 'firebase/ai';\nimport { FIREBASE_AI } from '../firebase/firebase-ai';\n\n@Injectable({ providedIn: 'root' })\nexport class AiService {\n  private readonly ai = inject(FIREBASE_AI);\n\n  async ask(prompt: string): Promise<string> {\n    const model = getGenerativeModel(this.ai, { model: 'gemini-3.5-flash' });\n    const result = await model.generateContent(prompt);\n    return result.response.text();\n  }\n}\n```\n\nGenerate a dedicated **AI Demo** component with the Angular CLI, or wire the call into your root **`App`** component.\n\n```\nng generate component ai-demo --inline-template --skip-tests\njs\nimport { Component, inject, signal } from '@angular/core';\nimport { AiService } from '../services/ai.service';\n\n@Component({\n  selector: 'app-ai-demo',\n  template: `\n    <button (click)=\"run()\" [disabled]=\"loading()\">\n      {{ loading() ? 'Please wait…' : 'Ask Gemini' }}\n    </button>\n    @if (error()) {\n      <p role=\"alert\">{{ error() }}</p>\n    }\n    @if (reply()) {\n      <pre>{{ reply() }}</pre>\n    }\n  `,\n  styleUrl: './ai-demo.scss',\n})\nexport class AiDemo {\n  private readonly ai = inject(AiService);\n  readonly loading = signal(false);\n  readonly reply = signal('');\n  readonly error = signal('');\n\n  async run(): Promise<void> {\n    this.loading.set(true);\n    this.error.set('');\n    this.reply.set('');\n    try {\n      this.reply.set(await this.ai.ask('Say hello in one sentence.'));\n    } catch (err) {\n      this.error.set(\n        err instanceof Error ? err.message : 'Something went wrong. Check the console.',\n      );\n    } finally {\n      this.loading.set(false);\n    }\n  }\n}\n```\n\nRender it from `src/app/app.html`:\n\n```\n<app-ai-demo />\n```\n\nImport **`AiDemo`** in `src/app/app.ts` — Angular components are standalone by default and must be listed in the parent `imports` array:\n\n``` js\nimport { Component } from '@angular/core';\nimport { AiDemo } from './ai-demo/ai-demo';\n\n@Component({\n  selector: 'app-root',\n  imports: [AiDemo],\n  templateUrl: './app.html',\n  styleUrl: './app.scss',\n})\nexport class App {}\n```\n\nWithout that import, `<app-ai-demo />` will fail at compile time with an unknown element error.\n\nRun `ng serve` and click on the **Ask Gemini** button in the rendered UI on [http://localhost:4200](http://localhost:4200)\n\nClick **Ask Gemini**. If you get text back — not a **403** — your setup works. That is the pass/fail check.\n\nOn first load, the console should show no `App Check token fetch failed`, and you should have registered the debug token from Part 2, Step 8. Optional: in DevTools → **Network**, filter `firebasevertexai` and confirm a **POST** to `:generateContent` returns **200**. Do not worry if you cannot spot `X-Firebase-AppCheck` — the header is easy to miss, and Gemini succeeding is what matters.\n\n**403 The caller does not have permission?** Re-run **AI Logic → Get started**, confirm the debug token is registered, confirm the Browser key allowlist includes **Firebase AI Logic API** + **Firebase App Check API**, then wait a few minutes and hard-refresh.\n\n| Feature | SDK entry point | \n|---|---|\n| Multi-turn chat | `startChat()` on a generative model | \n| Function calling | `tools` +`FunctionCallingConfig` on the model | \n| Streaming | `generateContentStream()` | \n| System instructions | `systemInstruction` in model config | \n\nFor a full function-calling example, see [ByteWise `ai.service.ts`](https://github.com/waynegakuo/bytewise/blob/main/src/app/services/ai.service.ts).\n\nWe are at an inflection point where developers can build intelligent web experiences, powered by Large Language Models. Firebase AI Logic provides this infrastructure with a free tier on the Gemini Developer API and Agent Platform Gemini API for enterprise-level use cases.\n\nIf you want these capabilities where latency matters, Firebase AI Logic will be your best bet, while offering you security and abuse prevention for your production apps.\n\nTo see Firebase AI Logic in action in e-commerce applications, check out [**Bytewise Shop**](https://bytewiseshop.web.app/), a fictional tech gadget shop I built to demonstrate the use of tool calling on actions such as **add to cart**, **check inventory**, **clearing cart**, all using **natural language**.\n\n`src/environments/environment.ts` — production values`src/environments/environment.development.ts` — add `appCheckDebugToken`\n`provideFirebaseAI()`\n`AiDemo` component + import in `app.ts`", "url": "https://wpnews.pro/news/firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend", "canonical_source": "https://dev.to/gde/firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend-54eh", "published_at": "2026-09-16 17:18:58+00:00", "updated_at": "2026-09-16 17:42:35.146181+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "developer-tools", "generative-ai", "large-language-models"], "entities": ["Firebase AI Logic", "Angular", "Gemini", "Google", "Vertex AI in Firebase", "App Check", "reCAPTCHA Enterprise", "ByteWise"], "alternates": {"html": "https://wpnews.pro/news/firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend", "markdown": "https://wpnews.pro/news/firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend.md", "text": "https://wpnews.pro/news/firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend.txt", "jsonld": "https://wpnews.pro/news/firebase-ai-logic-in-angular-client-side-gemini-without-a-custom-backend.jsonld"}}