GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-22Advanced

A Gemini API Control Plane for Indie Developers Running an App Portfolio

When you run several apps (wallpaper, healing, manifestation) on Gemini API, keys scatter and per-app cost attribution disappears. This is the three-layer control-plane architecture I have used for twelve months, with the traps that only show up over time.

Gemini API192Architecture9Indie Developer13AdMob9Cost Management5

Premium Article

Some time in 2026, seven of the apps I run as an indie developer — mostly wallpaper, healing, and manifestation apps — quietly tipped over into calling the Gemini API directly. The first few were built the obvious way: each app held its own API key and called Gemini inline. By the fourth one I had a small but unmistakable incident: I could no longer tell which app was responsible for last month's Gemini bill. That was the moment I stopped, sat down, and redesigned how the portfolio talked to the API.

If you operate several apps at once, the picture is probably familiar. Each app has its own key. Each app hard-codes a model name. The bill lands as one undifferentiated line on the Google Cloud invoice. From the outside everything looks fine; from the operator's seat the resolution of what you can see slowly fades to grey.

In the following sections I want to walk through the idea of a "Gemini API control plane" — a thin central layer between your apps and the Gemini API — based on twelve months of running it myself. The implementation I describe is deliberately small: Firebase Remote Config plus a lightweight relay (Cloud Functions or Cloudflare Workers), sized for an indie developer rather than an enterprise team.

Why a central layer at all

When you call the Gemini API directly from several apps, the problems converge on roughly three:

  1. You cannot roll out a new model in one move. Upgrading gemini-2.5-flash to gemini-3.1-flash requires shipping a new build of every app and waiting through every store review
  2. You cannot attribute cost per app. The GCP invoice is project-scoped, so you cannot tell what fraction of the bill came from the wallpaper app's onboarding text versus the manifestation app's daily message
  3. You cannot anomaly-detect per app. When call volume spikes, you have no way to know whether a single app is misbehaving or the whole portfolio is trending up

All three dissolve if you place a thin central layer between the apps and Gemini. The apps say "generate text" or "describe this image." The central layer chooses the model, tracks the cost, and enforces rate limits.

This is the same pattern that the commercial world calls an "LLM Gateway" or "Model Router." But for an indie developer running about 50 million cumulative downloads alone, full-fat middleware like Portkey or LiteLLM is overkill. Firebase Remote Config and Cloudflare Workers, in my experience, are enough.

Three layers: Config, Routing, Telemetry

The control plane's responsibilities split cleanly into three layers.

LayerResponsibilityImplementation
ConfigWhat settings each app seesFirebase Remote Config with conditional values
RoutingActual destination and fallback decisionsCloudflare Workers as a lightweight relay
Telemetryusage_metadata collection and daily aggregationBigQuery plus a Google Sheet for the per-app roll-up

Keeping these separate matters. If the boundary is fuzzy, rolling back a model change ends up meaning "edit the Worker source and redeploy" — which is too slow. In my setup, every layer can be changed without a deploy.

Config layer: Remote Config as a server-side setting store

Firebase Remote Config is usually discussed in the context of client-side A/B testing. Here I use it as a versioned server-side config store. It comes with built-in change history and instant rollback, which matches the cadence of indie development.

Concretely, each app gets one slot inside a single Remote Config parameter called gemini_app_config:

{
  "wallpaper_app": {
    "model": "gemini-3.1-flash",
    "fallback": "gemini-2.5-flash",
    "max_tokens": 1024,
    "rate_limit_per_minute": 30,
    "rollout_percent": 100,
    "budget_jpy_per_day": 800
  },
  "healing_app": {
    "model": "gemini-3.1-flash",
    "fallback": "gemini-2.5-flash",
    "max_tokens": 512,
    "rate_limit_per_minute": 20,
    "rollout_percent": 25,
    "budget_jpy_per_day": 300
  },
  "attraction_app": {
    "model": "gemini-2.5-flash",
    "fallback": "gemini-2.5-flash-lite",
    "max_tokens": 768,
    "rate_limit_per_minute": 15,
    "rollout_percent": 100,
    "budget_jpy_per_day": 200
  }
}

rollout_percent is the dial for the staged rollout described below, and budget_jpy_per_day feeds the circuit-breaker logic in the Telemetry section.

The Remote Config REST API requires an OAuth 2.0 admin token and has tight rate limits, so the Worker reads it with a five-minute cache:

// worker/config.ts
type AppConfig = {
  model: string;
  fallback: string;
  max_tokens: number;
  rate_limit_per_minute: number;
  rollout_percent: number;
  budget_jpy_per_day: number;
};
 
let configCache: { value: Record<string, AppConfig>; expiresAt: number } | null = null;
 
export async function getAppConfig(env: Env, appId: string): Promise<AppConfig> {
  if (configCache && configCache.expiresAt > Date.now()) {
    return configCache.value[appId];
  }
 
  const token = await getRemoteConfigAccessToken(env);
  const res = await fetch(
    `https://firebaseremoteconfig.googleapis.com/v1/projects/${env.GCP_PROJECT}/remoteConfig`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  const body = await res.json<RemoteConfigResponse>();
  const raw = body.parameters.gemini_app_config.defaultValue.value;
  const parsed = JSON.parse(raw) as Record<string, AppConfig>;
 
  configCache = { value: parsed, expiresAt: Date.now() + 5 * 60 * 1000 };
  return parsed[appId];
}

A subtle but important point: the Worker holds no "default" model name or budget. It only holds the decision logic. The values themselves always come from Remote Config. That separation is what makes future value-only changes painless.

Routing layer: dispatch and staged rollout

Apps call the Worker at /v1/generate and pass an appId. The Worker pulls the config, looks at rollout_percent, picks between the new model and the fallback, and forwards the call to Gemini.

// worker/router.ts
export async function generate(req: GenerateRequest, env: Env): Promise<GenerateResponse> {
  const cfg = await getAppConfig(env, req.appId);
 
  await assertWithinBudget(env, req.appId, cfg.budget_jpy_per_day);
  await assertRateLimit(env, req.appId, cfg.rate_limit_per_minute);
 
  const useNewModel = hashUser(req.userId) % 100 < cfg.rollout_percent;
  const model = useNewModel ? cfg.model : cfg.fallback;
 
  const t0 = Date.now();
  const result = await callGemini(env, model, req.prompt, cfg.max_tokens);
  const elapsed = Date.now() - t0;
 
  await emitTelemetry(env, {
    app_id: req.appId,
    model_used: model,
    rollout_branch: useNewModel ? "new" : "fallback",
    prompt_tokens: result.usageMetadata.promptTokenCount,
    output_tokens: result.usageMetadata.candidatesTokenCount,
    elapsed_ms: elapsed,
    timestamp: new Date().toISOString(),
  });
 
  return { text: result.text, model };
}

The critical line is hashUser(req.userId) % 100 < cfg.rollout_percent. A given user always lands on the same branch. Without that property, users feel a sudden change in response style mid-session, which reads as a bug to them.

In my setup, staged rollouts always go 5 → 25 → 100 and each step holds for at least 48 hours. Forty-eight hours covers at least one full cycle of late-night and daytime traffic, and is long enough to outlast AdMob's reporting delay (up to 24 hours).

Telemetry layer: aggregating usage_metadata for attribution

The usageMetadata returned by callGemini is the lifeline of per-app cost attribution. I land one row per call in BigQuery:

CREATE TABLE gemini_usage (
  timestamp TIMESTAMP NOT NULL,
  app_id STRING NOT NULL,
  model_used STRING NOT NULL,
  rollout_branch STRING NOT NULL,
  prompt_tokens INT64 NOT NULL,
  output_tokens INT64 NOT NULL,
  elapsed_ms INT64 NOT NULL
)
PARTITION BY DATE(timestamp);

The daily attribution query joins against a small gemini_pricing view that holds the per-million-token rates:

SELECT
  DATE(timestamp, "Asia/Tokyo") AS jst_date,
  app_id,
  model_used,
  SUM(prompt_tokens) AS in_tokens,
  SUM(output_tokens) AS out_tokens,
  ROUND(
    SUM(prompt_tokens) / 1000000.0 * p.input_usd_per_1m +
    SUM(output_tokens) / 1000000.0 * p.output_usd_per_1m,
    4
  ) AS usd_estimated
FROM gemini_usage u
JOIN gemini_pricing p USING (model_used)
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
GROUP BY jst_date, app_id, model_used, p.input_usd_per_1m, p.output_usd_per_1m
ORDER BY jst_date DESC, usd_estimated DESC;

A Google Apps Script triggers this query at 7 a.m. JST every morning and writes the results into a single sheet. Where the GCP invoice arrives once a month, this sheet arrives once a day — collapsing operational tempo from monthly to daily.

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
A Firebase Remote Config–based three-layer control plane (Config / Routing / Telemetry) with implementation templates
Per-app cost attribution via usage_metadata wrapping plus a daily BigQuery roll-up SQL
A 5%→25%→100% model rollout pattern that lives entirely inside Remote Config — no separate A/B tooling required
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.

or
Unlock all articles with Membership →
Share

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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API / SDK2026-05-27
Letting Gemini Flash Decide continue / pause / rollback for Staged Rollouts: An Indie Developer's Three-Signal Engine
How I built a Gemini Flash decision engine that reads Firebase Crashlytics, App Store / Google Play reviews, and AdMob revenue together, and outputs continue / pause / rollback for each staged rollout across six indie apps. Numbers from two months of production use included.
API / SDK2026-07-18
Keeping a Long-Running Managed Agent Alive Across Sandbox Recycling — Durable Checkpoints and Idempotent Resume
A Managed Agents sandbox can be recycled out from under you. Before 40 minutes of work resets to zero, we design a durable checkpoint that pushes progress outside the sandbox and an idempotent resume that never runs a side effect twice. With working SQLite code.
API / SDK2026-06-27
Stopping Runaway Costs Twice: Project Spend Caps Plus an App-Side Soft Limit
Pairing Gemini API Project Spend Caps (a monthly USD ceiling) with an app-side soft circuit breaker that trips before the hard cap. Includes a working Python and sqlite daily cost ledger.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →