<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Firebase ai logic - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Firebase ai logic - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 21 Sep 2026 13:06:58 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/firebase-ai-logic/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Why You Should Never Embed Your Gemini API Key in Client Code (And How Firebase AI Logic Fixes It) ]]>
                </title>
                <description>
                    <![CDATA[ The explosion of generative AI has pushed thousands of web developers to add intelligent features to their apps. The first instinct is usually to call the Gemini API's SDK directly from the browser. T ]]>
                </description>
                <link>https://www.freecodecamp.org/news/why-you-should-never-embed-your-gemini-api-key-in-client-code-and-how-firebase-ai-logic-fixes-it/</link>
                <guid isPermaLink="false">6ab11bd3c0a4b6fb82bc1891</guid>
                
                    <category>
                        <![CDATA[ Firebase ai logic ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firebase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ firebase app check ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Caleb Mintoumba ]]>
                </dc:creator>
                <pubDate>Mon, 21 Sep 2026 11:58:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3a42885a-9cac-4fbc-8fac-affbafd7785c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The explosion of generative AI has pushed thousands of web developers to add intelligent features to their apps.</p>
<p>The first instinct is usually to call the Gemini API's SDK directly from the browser. That instinct comes with a serious security risk: exposing your API key to the world.</p>
<p>In this article, you'll learn why shipping a raw Gemini API key to the client is dangerous, how Firebase AI Logic's proxy architecture solves it, and how Firebase App Check closes the second half of the problem that a proxy alone doesn't fix.</p>
<p>By the end, you'll have a working, production-style setup: a protected AI Logic client, a properly configured App Check flow (debug token included), and real usage patterns, streaming, multi-turn chat, and structured JSON output, not just a single <code>console.log</code>.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-problem-with-client-side-api-keys">The Problem With Client-Side API Keys</a></p>
</li>
<li><p><a href="#heading-step-1-how-firebase-ai-logics-proxy-architecture-works">Step 1 – How Firebase AI Logic's Proxy Architecture Works</a></p>
</li>
<li><p><a href="#heading-step-2-what-firebase-app-check-actually-does">Step 2 – What Firebase App Check Actually Does</a></p>
</li>
<li><p><a href="#heading-step-3-set-up-your-firebase-project">Step 3 – Set Up Your Firebase Project</a></p>
</li>
<li><p><a href="#heading-step-4-integrate-firebase-app-check">Step 4 – Integrate Firebase App Check</a></p>
</li>
<li><p><a href="#heading-step-5-implement-firebase-ai-logic">Step 5 – Implement Firebase AI Logic</a></p>
</li>
<li><p><a href="#heading-debugging-common-issues">Debugging Common Issues</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before you start, make sure you have the following:</p>
<ul>
<li><p><strong>Node.js v18 or later</strong> (<code>node --version</code>)</p>
</li>
<li><p>A <strong>Google account</strong> to create a Firebase project (the free Spark plan works for the Gemini Developer API)</p>
</li>
<li><p>Basic familiarity with JavaScript, <code>async</code>/<code>await</code>, and ES modules</p>
</li>
<li><p>A code editor and a terminal</p>
</li>
</ul>
<p>You don't need prior experience with Firebase, App Check, or the Gemini API, as this guide builds that understanding from the ground up.</p>
<h2 id="heading-the-problem-with-client-side-api-keys">The Problem With Client-Side API Keys</h2>
<p>Embedding an API key inside a JavaScript bundle, or in an <code>.env</code> file that ends up shipped to the browser, is a critical security flaw, and it's trivially easy to exploit. Here's what that actually looks like in practice.</p>
<p>Say you call the Gemini API directly from client code like this:</p>
<pre><code class="language-javascript">// DON'T do this in a browser-shipped app
const genAI = new GoogleGenerativeAI("AIzaSyD4-your-real-key-here");
</code></pre>
<p>Bundle that with any build tool and the key lands in your output JS as plain text. Anyone can find it in under a minute, no special tools required:</p>
<pre><code class="language-shell"># Anyone can run this against your deployed bundle
curl -s https://your-app.com/assets/main.js | grep -oE "AIzaSy[A-Za-z0-9_-]{33}"
</code></pre>
<p>That one command extracts a Gemini API key from a minified production bundle if it's in there. From the Network tab in the browser's dev tools, it's even more visible, every outgoing request to <code>generativelanguage.googleapis.com</code> shows the key directly in the query string or headers.</p>
<p>If your Gemini API key leaks this way, an attacker can:</p>
<ul>
<li><p>Drain your entire usage quota</p>
</li>
<li><p>Cause your Cloud bill to spike unpredictably (Gemini calls are billed per token, unlike a flat-rate database read)</p>
</li>
<li><p>Use your resources to run their own requests, which can get your Google Cloud project suspended for abuse</p>
</li>
</ul>
<p>Historically, the only fix was to build, deploy, and maintain a custom backend server (Node.js, Python, Go...) that acted as a proxy between your app and the Gemini API, just to keep one string secret. That's real infrastructure to run for what should be a simple feature.</p>
<h2 id="heading-step-1-how-firebase-ai-logics-proxy-architecture-works">Step 1 – How Firebase AI Logic's Proxy Architecture Works</h2>
<p><strong>Firebase AI Logic</strong> gives you that proxy gateway without you having to build or host it. You still write client-side code, but the key never leaves Google's infrastructure.</p>
<pre><code class="language-plaintext">[Web Browser] ──(Authenticated Request)──&gt; [Firebase AI Logic Proxy] ──(Key Injected Server-Side)──&gt; [Gemini API]
</code></pre>
<p>Your Gemini API key stays stored securely, tied to your Firebase project. The client SDK sends a request to the proxy gateway, and the proxy injects the key and forwards the call to your chosen "Gemini API" provider. The key is never present in your JS bundle, your network requests, or anything the browser can inspect.</p>
<p>Firebase AI Logic supports two providers, chosen when you set up the service in the console:</p>
<table>
<thead>
<tr>
<th></th>
<th>Gemini Developer API</th>
<th>Agent Platform Gemini API (formerly Vertex AI)</th>
</tr>
</thead>
<tbody><tr>
<td>Billing plan</td>
<td>Works on the free Spark plan</td>
<td>Requires the Blaze (pay-as-you-go) plan</td>
</tr>
<tr>
<td>Best for</td>
<td>Getting started fast, prototyping, most web/mobile apps</td>
<td>Data-residency requirements, teams already on Google Cloud/Vertex AI</td>
</tr>
<tr>
<td>Region control</td>
<td>Limited</td>
<td>Choose specific regions (<code>us</code>, <code>eu</code>) for model access, depending on the model</td>
</tr>
<tr>
<td>Setup friction</td>
<td>Minimal, no billing needed</td>
<td>Requires linking a Cloud Billing account</td>
</tr>
</tbody></table>
<p>For most apps, start with the Gemini Developer API, that's what this tutorial uses. Switching providers later is a config change, not a code rewrite, since both go through the same <code>getGenerativeModel()</code> interface.</p>
<h2 id="heading-step-2-what-firebase-app-check-actually-does">Step 2 – What Firebase App Check Actually Does</h2>
<p>A proxy hides the key. It does not, by itself, stop <em>anyone</em> from calling that proxy. Your Firebase config object (<code>apiKey</code>, <code>projectId</code>, and so on) isn't a secret, it's meant to be public, and it's visible in every deployed app's bundle by design. Without another layer, a bot could copy that config, initialize its own Firebase app pointed at your project, and call your AI Logic proxy directly, running up your Gemini bill with none of your actual users involved.</p>
<p>This is exactly what <strong>Firebase App Check</strong> is for, and it's worth understanding precisely what it does, because it's easy to confuse with authentication.</p>
<p><strong>App Check is attestation, not authentication.</strong> Firebase Authentication answers "who is this user?" App Check answers a different question: "is this request coming from a genuine, untampered instance of <em>my app</em>, and not a script, a bot, or someone else's app using my config?" You can, and should, use both together, but App Check is what protects you from abuse even when a request comes with zero user context.</p>
<p><strong>How the flow actually works, step by step:</strong></p>
<ol>
<li><p>When your app initializes App Check, the SDK triggers an <strong>attestation challenge</strong> with the configured provider. On the web, that's reCAPTCHA Enterprise, it runs an invisible risk assessment (mouse movement, browser fingerprint, network signals) and returns a token asserting "this looks like a legitimate browser session."</p>
</li>
<li><p>Your app's SDK sends that reCAPTCHA token to Firebase's App Check backend, which exchanges it for a <strong>Firebase App Check token</strong>, a short-lived, signed JWT.</p>
</li>
<li><p>That App Check token is cached locally and automatically attached to every subsequent request your app makes to Firebase AI Logic (and other App Check-integrated services like Firestore or Cloud Functions).</p>
</li>
<li><p>Before the AI Logic proxy forwards your request to Gemini, it verifies the App Check token's signature and validity. No valid token, no request reaches the model.</p>
</li>
</ol>
<p>The reason this matters <em>specifically</em> for generative AI, more than for a typical CRUD backend, is cost shape. A blocked Firestore read costs you nothing. A blocked Gemini call, if it weren't blocked, could cost real money per request, and at scale, a scripted abuse loop can burn through a monthly budget in hours. App Check is the gate that makes sure only your actual app can trigger that spend.</p>
<p><strong>Heads up:</strong> Google has announced that App Check enforcement will become <strong>mandatory for Firebase AI Logic starting November 2, 2026</strong>. Starting even earlier, in July 2026, the guided setup workflow in the Firebase console already enables App Check automatically for new AI Logic integrations. Any unverified request made after the enforcement date will be rejected outright, so it's worth wiring this up now rather than scrambling later.</p>
<h2 id="heading-step-3-set-up-your-firebase-project">Step 3 – Set Up Your Firebase Project</h2>
<ol>
<li><p>Go to the <a href="https://console.firebase.google.com/">Firebase console</a> and create a new project (or open an existing one).</p>
</li>
<li><p>In the left sidebar, open <strong>Build</strong> and then <strong>AI Logic</strong>, then click <strong>Get started</strong>. Choose <strong>Gemini Developer API</strong> as your provider to follow along without setting up billing.</p>
</li>
<li><p>Back in <strong>Project settings</strong>, go to <strong>General</strong> and then <strong>Your apps</strong>. Register a web app if you haven't already, and copy the Firebase config object. You'll need it in the next steps.</p>
</li>
</ol>
<p>We'll leave the App Check setup for the next step, as it deserves its own walkthrough.</p>
<h2 id="heading-step-4-integrate-firebase-app-check">Step 4 – Integrate Firebase App Check</h2>
<h3 id="heading-register-your-app-for-recaptcha-enterprise">Register Your App for reCAPTCHA Enterprise</h3>
<p>In the Firebase console, go to <strong>Build</strong> and then <strong>App Check</strong>, select your web app, and choose <strong>reCAPTCHA Enterprise</strong> as the provider. Firebase generates a <strong>site key</strong> tied to your app's domain. Copy it, as you'll pass it into your code below.</p>
<h3 id="heading-install-the-sdk">Install the SDK</h3>
<pre><code class="language-shell">npm install firebase
</code></pre>
<p>That's the only package you need. <code>ReCaptchaEnterpriseProvider</code> ships inside <code>firebase/app-check</code>, part of the same <code>firebase</code> package. There's no separate reCAPTCHA SDK to install and no <code>&lt;script&gt;</code> tag to add manually. Firebase loads the reCAPTCHA Enterprise script for you as soon as <code>initializeAppCheck()</code> runs.</p>
<h3 id="heading-initialize-app-check-with-a-debug-token-for-local-development">Initialize App Check with a Debug Token for Local Development</h3>
<p>reCAPTCHA Enterprise doesn't behave reliably on <code>localhost</code>, so before writing production code, set up the App Check <strong>debug provider</strong>. This lets you develop locally without fighting false rejections:</p>
<pre><code class="language-javascript">// app-check-setup.js
import { initializeApp } from "firebase/app";
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from "firebase/app-check";

const firebaseConfig = {
  apiKey: "AIzaSy...",
  authDomain: "your-project.firebaseapp.com",
  projectId: "your-project",
  storageBucket: "your-project.firebasestorage.app",
  messagingSenderId: "123456789",
  appId: "1:1234:web:abcd",
};

const app = initializeApp(firebaseConfig);

// Enable the debug provider ONLY in local/dev environments.
// This prints a debug token to the console the first time it runs.
if (location.hostname === "localhost") {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

export const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaEnterpriseProvider("YOUR_RECAPTCHA_ENTERPRISE_SITE_KEY"),
  isTokenAutoRefreshEnabled: true,
});

export { app };
</code></pre>
<p>The first time this runs locally, check your browser console for a line like:</p>
<pre><code class="language-plaintext">App Check debug token: 5f2b1a3c-....-....-.... You will need to add it to your app's App Check settings in the Firebase console before the token can be used.
</code></pre>
<p>Copy that token into <strong>App Check → Apps → [your app] → Manage debug tokens</strong> in the console. From then on, requests from your local machine are treated as verified, without needing a real reCAPTCHA challenge. Never ship this debug-token block to production. You should gate it behind an environment check as shown above.</p>
<h3 id="heading-verifying-its-working">Verifying it's Working</h3>
<p>Once your app makes its first App Check-protected request (you'll wire that up in Step 5), go to <strong>App Check</strong> and then <strong>APIs</strong> in the console. You'll see a live breakdown of verified vs. unverified requests hitting Firebase AI Logic. If everything is wired correctly, your traffic shows up as "Verified."</p>
<h2 id="heading-step-5-implement-firebase-ai-logic">Step 5 – Implement Firebase AI Logic</h2>
<p>With App Check in place, here's how to actually use the model, beyond a single request/response round trip.</p>
<h3 id="heading-basic-setup">Basic Setup:</h3>
<pre><code class="language-javascript">// ai-client.js
import { getAI, GoogleAIBackend, getGenerativeModel } from "firebase/ai";
import { app } from "./app-check-setup.js";

// useLimitedUseAppCheckTokens issues short-lived tokens for extra protection
// against replay attacks on top of standard App Check verification.
const ai = getAI(app, {
  backend: new GoogleAIBackend(),
  useLimitedUseAppCheckTokens: true,
});

export const model = getGenerativeModel(ai, { model: "gemini-3.8-flash" });
</code></pre>
<p><strong>Note:</strong> Gemini model names and availability change frequently, <code>gemini-2.0-flash</code> and its Lite variant were retired on June 1, 2026, and the Gemini 2.5 line is now deprecated in favor of the Gemini 3.x series.</p>
<p>Always check the <a href="https://firebase.google.com/docs/ai-logic/models">supported models page</a> before hardcoding a model name in production. Or better yet, load it from Firebase Remote Config so you can swap models without shipping a new build.</p>
<h3 id="heading-example-1-a-single-request">Example 1 — a Single Request</h3>
<pre><code class="language-javascript">import { model } from "./ai-client.js";

async function generateAIText(prompt) {
  try {
    const result = await model.generateContent(prompt);
    const response = result.response;
    return response.text();
  } catch (error) {
    console.error("Firebase AI Logic request failed:", error);
  }
}

generateAIText("Explain the purpose of an API proxy in two sentences.");
</code></pre>
<h3 id="heading-example-2-streaming-into-the-ui">Example 2 — Streaming into the UI</h3>
<p>For anything longer than a sentence, streaming gives users a response that starts appearing immediately instead of a multi-second blank wait. Here's a real DOM-wired example, not just a console log:</p>
<pre><code class="language-javascript">import { model } from "./ai-client.js";

async function streamIntoElement(prompt, targetElement) {
  targetElement.textContent = "";

  const result = await model.generateContentStream(prompt);

  for await (const chunk of result.stream) {
    targetElement.textContent += chunk.text();
  }

  // The aggregated final response is also available once streaming finishes
  const finalResponse = await result.response;
  console.log("Total tokens used:", finalResponse.usageMetadata?.totalTokenCount);
}

const output = document.querySelector("#ai-output");
streamIntoElement("Write a 3-sentence product description for a smart water bottle.", output);
</code></pre>
<h3 id="heading-example-3-multi-turn-chat">Example 3 — Multi-turn Chat</h3>
<p>For a chatbot-style feature, you don't want to manually track and resend the whole conversation on every call. <code>startChat()</code> handles that for you:</p>
<pre><code class="language-javascript">import { model } from "./ai-client.js";

const chat = model.startChat({
  history: [
    { role: "user", parts: [{ text: "I'm building a task management app." }] },
    { role: "model", parts: [{ text: "Got it, what would you like help with?" }] },
  ],
});

async function sendChatMessage(message) {
  const result = await chat.sendMessage(message);
  return result.response.text();
}

sendChatMessage("Suggest 3 status labels for a Kanban board.");
// A follow-up call automatically has the prior turns as context:
sendChatMessage("Now suggest color codes for each of those.");
</code></pre>
<h3 id="heading-example-4-structured-json-output">Example 4 — Structured JSON Output</h3>
<p>If you're feeding the model's output into your app's logic (rendering a card, populating a form), free-text output is fragile to parse. Pass a <code>responseSchema</code> to force valid, typed JSON back:</p>
<pre><code class="language-javascript">import { getGenerativeModel, Schema } from "firebase/ai";
import { ai } from "./ai-client.js"; // assuming `ai` is also exported from ai-client.js

const taskSchema = Schema.object({
  properties: {
    title: Schema.string(),
    priority: Schema.enumString({ enum: ["low", "medium", "high"] }),
    tags: Schema.array({ items: Schema.string() }),
  },
});

const structuredModel = getGenerativeModel(ai, {
  model: "gemini-3.8-flash",
  generationConfig: {
    responseMimeType: "application/json",
    responseSchema: taskSchema,
  },
});

async function extractTaskFromText(text) {
  const result = await structuredModel.generateContent(
    `Extract a task from this note: "${text}"`
  );
  return JSON.parse(result.response.text());
}

extractTaskFromText("Need to review the PR from Sarah by Friday, this is urgent");
// → { title: "Review PR from Sarah", priority: "high", tags: ["review"] }
</code></pre>
<h3 id="heading-example-5-handling-rate-limits-and-transient-errors">Example 5 — Handling Rate Limits and Transient Errors</h3>
<p>Gemini calls can hit rate limits or transient failures, especially under load. A simple exponential backoff keeps your app resilient without hammering the API:</p>
<pre><code class="language-javascript">import { model } from "./ai-client.js";

async function generateWithRetry(prompt, maxRetries = 3) {
  for (let attempt = 0; attempt &lt;= maxRetries; attempt++) {
    try {
      const result = await model.generateContent(prompt);
      return result.response.text();
    } catch (error) {
      const isRetryable = error.message?.includes("429") || error.message?.includes("503");
      if (!isRetryable || attempt === maxRetries) throw error;

      const delayMs = 2 ** attempt * 1000; // 1s, 2s, 4s...
      await new Promise((resolve) =&gt; setTimeout(resolve, delayMs));
    }
  }
}
</code></pre>
<h2 id="heading-debugging-common-issues">Debugging Common Issues</h2>
<h3 id="heading-issue-1-api-key-not-valid-please-pass-a-valid-api-key">Issue 1: <code>API key not valid. Please pass a valid API key.</code></h3>
<p>This usually means the <code>apiKey</code> in your Firebase config doesn't match your project, or the required APIs weren't enabled. Double-check the value against <strong>Project settings → General</strong> in the console.</p>
<h3 id="heading-issue-2-403-or-requests-silently-blocked-even-though-your-code-looks-correct">Issue 2: <code>403</code> or requests silently blocked, even though your code looks correct</h3>
<p>Almost always an App Check registration mismatch. Confirm that the domain you're testing from matches what you registered for the reCAPTCHA Enterprise site key, and that App Check shows your requests as "Verified" (not "Unenforced" or missing entirely) under <strong>App Check</strong> and then <strong>APIs</strong>.</p>
<h3 id="heading-issue-3-app-check-works-in-production-but-fails-on-localhost">Issue 3: App Check works in production but fails on <code>localhost</code></h3>
<p>Expected, reCAPTCHA Enterprise doesn't run reliably on <code>localhost</code>. Make sure the debug-token block from Step 4 is active in your dev environment, and that you've added the printed debug token to <strong>App Check</strong> and then <strong>Manage debug tokens</strong> in the console. If you rotate machines or clear browser storage, a fresh token gets printed, and you'll need to re-register it.</p>
<h3 id="heading-issue-4-model-errors-or-unexpected-shutdowns">Issue 4: Model errors or unexpected shutdowns</h3>
<p>Google periodically retires older Gemini models with a few months' notice, <code>gemini-2.0-flash</code> and its Lite variant, for instance, were shut down on June 1, 2026. If a request that used to work suddenly returns a 404, check the <a href="https://firebase.google.com/docs/ai-logic/models">models page</a> for a deprecation notice before assuming it's a bug in your code.</p>
<h3 id="heading-issue-5-streaming-stops-partway-with-no-error">Issue 5: Streaming stops partway with no error</h3>
<p>This is usually a <code>usageMetadata</code>/token-limit issue, not a network failure. Check <code>finalResponse.candidates[0].finishReason</code> (available once <code>result.response</code> resolves), a value like <code>MAX_TOKENS</code> tells you the response was cut off, not that something crashed.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>Building generative AI features means adopting production-grade security from the prototyping stage, not bolting it on afterward. Firebase AI Logic's proxy architecture keeps your Gemini API key out of client code entirely, and Firebase App Check makes sure that even with the key hidden, only genuine instances of your app can spend your quota. Together, they cover the two failure modes that matter most: key theft and scripted abuse.</p>
<p>A few things worth exploring next:</p>
<ul>
<li><p><strong>Function calling / tool use</strong>, so Gemini can call your own app functions as part of its response</p>
</li>
<li><p><strong>Server-side prompt templates</strong>, if you want to keep your prompts out of client code entirely, not just your API key</p>
</li>
<li><p><strong>Hybrid inference</strong>, which falls back to on-device models in supported browsers when available, cutting cost and latency for simple requests</p>
</li>
<li><p><strong>Firebase Remote Config for model names</strong>, so you can roll out a new Gemini model to users without shipping a new app version</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
