◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
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 API249Architecture10Indie 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-07-27
Retiring Prompt Assets That Refuse to Die — A Ledger for Paths Static Analysis Cannot See
A prompt I had supposedly consolidated away was still running daily. Here is the retirement ledger I built to cross-check static references, flag defaults, observation windows, model end-of-life dates, and cache effectiveness.
◈ API / SDK2026-05-26
Running Gemini 3 Pro and Flash in Parallel on AdMob Monthly Reports — Notes from Ten Mornings
Implementation notes from sending the same AdMob monthly report to Gemini 3 Pro and Gemini 3 Flash in parallel for ten days. Cost, latency, and summary quality compared from an indie developer's perspective.
◈ API / SDK2026-05-21
Designing Event-Driven AI Workflows with Gemini API and Cloud Pub/Sub — Notes from an Indie Developer
An implementation memo on wiring Gemini API into Cloud Pub/Sub event-driven workflows. Using an app-review analysis pipeline as the running example, the article covers retry policy, dead-lettering, idempotency, and cost guardrails — from the perspective of someone running it solo.