●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Building Serverless AI Apps with Firebase Genkit and Gemini — An Implementation Notebook
A hands-on implementation notebook for building serverless AI apps with Firebase Genkit and Gemini. Flows, agents, and RAG on the current Genkit 1.x API, plus Cloud Functions deployment, cost control through model routing, and cold-start tuning from an indie developer's perspective.
Shipping one Flow, and why the framework's thinness matters
When I first picked up Firebase Genkit, I was wary. Running Dolice Labs as an indie developer, every new abstraction layer is something to learn, and a tool that doesn't earn its keep becomes a liability.
That wariness lifted when I ran a tiny Flow — one that just returns a greeting — in the local Dev UI, and then deployed the exact same code to Cloud Functions. Input and output schemas, local testing, tracing, deployment: all of it sits inside one consistent way of writing. Because the framework is thin, you get to focus on the Gemini call itself.
This is an implementation notebook that walks from that first Flow all the way to RAG and agents, using code I actually wrote and verified. It isn't a line-by-line translation of the docs; it centers on the places an indie developer tends to get stuck. Note that this is a fast-moving area — the examples here assume the 1.x genkit() constructor and zod schemas.
Install and initialize — consolidate in one place
Install Genkit and the Google AI plugin. TypeScript is the natural choice.
Consolidate initialization into a single file and import ai from it everywhere else, so swapping models is a one-line change.
// src/genkit.tsimport { genkit } from "genkit";import { googleAI } from "@genkit-ai/googleai";// Swap the model ID for whichever you want (a current Flash / Pro model)export const ai = genkit({ plugins: [googleAI({ apiKey: process.env.GOOGLE_API_KEY })], model: googleAI.model("gemini-2.5-flash"),});
Never hardcode apiKey; always pass it from the environment. Use .env locally, and the Secret Manager path shown below in production.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Copy-runnable Flow, Tool, and RAG code written against the current Genkit 1.x API (the genkit constructor and zod schemas)
✦Concrete patterns for cutting API cost with model routing and caching, plus how to make per-flow spend visible
✦Cold-start tuning from real operation: when to set minimum instances and how to move heavy init off the request path
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
A Genkit Flow bundles "input schema, processing, output schema" into one function. Writing the schema in zod lets the Dev UI generate an input form for you, and keeps execution traces typed.
// src/flows/greeting.tsimport { z } from "genkit";import { ai } from "../genkit";export const greetingFlow = ai.defineFlow( { name: "greeting", inputSchema: z.object({ name: z.string(), language: z.enum(["en", "ja"]), }), outputSchema: z.string(), }, async ({ name, language }) => { const prompt = language === "en" ? `Write a short, friendly greeting for ${name}.` : `${name}さんへの、短くて親切な挨拶を日本語で書いてください。`; const { text } = await ai.generate({ prompt }); return text; },);
Verify locally from the Dev UI.
# Start the Dev UI (try flows at http://localhost:4000)npx genkit start -- npx tsx --watch src/index.ts
The important detail here is reading text via destructuring from the return of ai.generate() — not a method call like the response.text() you may see in older samples. In Genkit 1.x the return value is a property, and that difference is the first thing that trips up a copy-and-paste.
Multi-step Flow — fold document analysis into one function
Folding several preprocessing steps and a generation call into a single Flow means the caller only has to pass input. Here we take a document and return a summary, sentiment, or keywords.
// src/flows/documentAnalysis.tsimport { z } from "genkit";import { ai } from "../genkit";export const documentAnalysisFlow = ai.defineFlow( { name: "documentAnalysis", inputSchema: z.object({ documentText: z.string(), analysisType: z.enum(["summary", "sentiment", "keywords"]), }), outputSchema: z.object({ analysisType: z.string(), result: z.string(), }), }, async ({ documentText, analysisType }) => { // Step 1: clamp the input to a safe length const cleaned = documentText.trim().slice(0, 5000); // Step 2: prepare a prompt per analysis type const prompts: Record<string, string> = { summary: `Summarize this document in three sentences:\n${cleaned}`, sentiment: `Rate the sentiment as negative / neutral / positive:\n${cleaned}`, keywords: `List the five most important keywords:\n${cleaned}`, }; // Step 3: generate with Gemini and return a structured result const { text } = await ai.generate({ prompt: prompts[analysisType] }); return { analysisType, result: text }; },);
Clamping the input at 5,000 characters guards against an unexpectedly long body spiking your token cost. Set the limit to match your documents, but keeping it on the outside removes one class of accidents.
Tool Calling and agents — hand tool choice to the model
In Genkit you define a tool with ai.defineTool and simply pass it to generate; the model decides when to call it. You don't build a special class to get an agent.
// src/tools/weather.tsimport { z } from "genkit";import { ai } from "../genkit";export const getWeather = ai.defineTool( { name: "getWeather", description: "Return the current weather for a given city", inputSchema: z.object({ city: z.string() }), outputSchema: z.object({ city: z.string(), temperature: z.number(), condition: z.string(), }), }, async ({ city }) => { // In production, call a real weather API here return { city, temperature: 25, condition: "Sunny" }; },);
// src/flows/assistant.tsimport { z } from "genkit";import { ai } from "../genkit";import { getWeather } from "../tools/weather";export const assistantFlow = ai.defineFlow( { name: "assistant", inputSchema: z.object({ question: z.string() }), outputSchema: z.string(), }, async ({ question }) => { const { text } = await ai.generate({ prompt: question, tools: [getWeather], system: "You are a capable assistant. Use tools only when needed, and answer concisely.", }); return text; },);
The more tools you add, the more room the model has to pick the wrong one at the wrong time. A tool's description is the model's only basis for that decision; vague wording drives up misfires. I've written up how to measure and fix this in instrumenting agent tool misselection.
RAG — connect to Firestore Vector Search
In RAG you turn the query into an embedding, retrieve the nearest documents, and answer grounded only in that context. Genkit handles embeddings through ai.embed.
// src/flows/rag.tsimport { z } from "genkit";import { ai } from "../genkit";import { googleAI } from "@genkit-ai/googleai";export const ragFlow = ai.defineFlow( { name: "documentRAG", inputSchema: z.object({ query: z.string(), topK: z.number().default(3) }), outputSchema: z.string(), }, async ({ query, topK }) => { // 1. Turn the query into an embedding vector const [embedding] = await ai.embed({ embedder: googleAI.embedder("text-embedding-004"), content: query, }); // 2. Retrieve nearest documents via Firestore Vector Search const docs = await searchSimilar(embedding.embedding, topK); // 3. Build a prompt grounded only in the retrieved context const context = docs.map((d) => `- ${d.content}`).join("\n"); const { text } = await ai.generate({ prompt: `Answer concisely, grounded only in the reference material below. If there is no basis, say so.\n\nReference:\n${context}\n\nQuestion: ${query}`, }); return text; },);// The Firestore nearest-neighbor search lives in a separate articleasync function searchSimilar( _embedding: number[], _topK: number,): Promise<Array<{ content: string }>> { return [];}
Deploy to Cloud Functions — wrap it thinly with onCallGenkit
A Flow deploys to Firebase Functions as-is. onCallGenkit keeps the wiring for auth, streaming, and App Check short.
// functions/src/index.tsimport { onCallGenkit } from "firebase-functions/https";import { defineSecret } from "firebase-functions/params";import { greetingFlow } from "./flows/greeting";import { assistantFlow } from "./flows/assistant";// Inject the API key from Secret Managerconst apiKey = defineSecret("GOOGLE_API_KEY");export const greeting = onCallGenkit({ secrets: [apiKey] }, greetingFlow);export const assistant = onCallGenkit({ secrets: [apiKey] }, assistantFlow);
The deployment steps:
# 1. Install the Firebase CLInpm install -g firebase-tools# 2. Register the API key in Secret Manager (keep it out of code)firebase functions:secrets:set GOOGLE_API_KEY# 3. Deploy only Functionsfirebase deploy --only functions# 4. Verifyfirebase functions:list
Receiving the API key through defineSecret is the key point. Inlining an environment variable creates a leak path into your repo or logs. Keeping it in Secret Manager also lets you rotate keys without a code change. For larger production workloads, deploying to Vertex AI Agent Engine is another option.
Cost optimization — model routing and making spend visible
Serverless billing quietly balloons through a single flow if you let it. The first thing I add is routing that sends work to a model based on difficulty.
// src/flows/routed.tsimport { z } from "genkit";import { ai } from "../genkit";import { googleAI } from "@genkit-ai/googleai";const FLASH = googleAI.model("gemini-2.5-flash");const PRO = googleAI.model("gemini-2.5-pro");export const routedFlow = ai.defineFlow( { name: "routed", inputSchema: z.object({ prompt: z.string(), hard: z.boolean() }), outputSchema: z.string(), }, async ({ prompt, hard }) => { // Send only hard tasks to Pro; handle the rest on Flash const { text } = await ai.generate({ model: hard ? PRO : FLASH, prompt }); return text; },);
Alongside that, log call counts and duration per flow. Watching only the grand total hides which flow is driving the bill. On my own setup I was running a simple classification task on Pro for a while; dropping it to Flash roughly halved that flow's model cost. My broader take on staged cost control is in cost guardrails for indie developers.
Cold starts and operation — the unglamorous tuning that paid off
The first wall in serverless was cold starts. The less often a flow is called, the slower its first invocation, and to a user that reads as a heavy app. Here are my own measurements.
Flow
Warm average
Cold-start first call
First call after min instances
greeting
~0.9s
~4.2s
~1.1s
documentAnalysis
~1.6s
~5.0s
~1.7s
documentRAG
~2.3s
~6.1s
~2.4s
The fixes are plain. Set minInstances to 1 on high-traffic paths to keep them warm, and move heavy initialization off module load and outside the request path. There's nothing flashy here, but perceived speed really does come down to this kind of tuning. Since minimum instances mean continuous billing, the indie compromise is to warm only the paths that genuinely need it.
Wrapping up — start small, and grow with the spend breakdown in hand
Genkit's value is that the framework is thin, letting you focus on the Gemini call. Start by running a single greeting Flow locally and deploying it. Once that one goes through, Tool Calling and RAG are the same way of writing, extended.
As a next step, record from day one the material you need to judge whether each flow warrants minInstances — its per-flow latency and call frequency. It's far easier to grow with that in hand than to add it later.
I hope this gives you a foothold for your own build. Thank you for reading.
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.