Firebase AI Logic (the SDK formerly shipped as Vertex AI in Firebase) solved the embarrassing part of putting a model in a client app: the API key no longer sits in your bundle. Requests go from the app to a Google-operated proxy, which attaches credentials and forwards them to Gemini.
That is an authentication fix, not an authorization fix. The proxy will happily forward the ten-thousandth request from the same device. Model tokens are the most expensive thing per call in a typical Firebase bill — a Firestore read is measured in microdollars, a long-context generation is not — so the first abusive user costs more than a month of your database traffic.
We have now cleaned up this failure mode three times. The shape is always the same: a client-side model call, App Check in monitoring mode, no per-user ceiling, and a surprise on the invoice. Here is the stack that stops it.
1. App Check enforced, not monitored
App Check is the only thing standing between your proxied model endpoint and a script with a stolen Firebase config. Firebase config values are public by design; treat them as such.
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { getAI, getGenerativeModel, GoogleAIBackend } from 'firebase/ai';
initializeAppCheck(app, {
provider: new ReCaptchaEnterpriseProvider(SITE_KEY),
isTokenAutoRefreshEnabled: true,
});
const ai = getAI(app, { backend: new GoogleAIBackend() });
const model = getGenerativeModel(ai, { model: 'gemini-2.5-flash' });
Initialize App Check before anything else, and check the console: the Firebase AI Logic API has its own enforcement toggle. Teams routinely enforce on Firestore, leave the AI endpoint in monitoring, and assume they are covered. Look at the verified-vs-unverified split for that specific API for a week before you flip it, then flip it. Our staged-rollout procedure is in Rolling Out App Check Without Locking Out Real Users.
What App Check gives you: requests come from builds of your app. What it does not give you: any limit on how many requests a legitimate build makes. A rooted device with a valid attestation is still a valid attestation.
2. A per-user ceiling that lives server-side
Client-side throttling is a courtesy, not a control. If the spend ceiling matters, the call has to pass through code you own. For anything beyond a demo we route generation through a callable Gen 2 function and count usage in Firestore.
import { onCall, HttpsError } from 'firebase-functions/v2/https';
export const generate = onCall(
{ enforceAppCheck: true, region: 'us-central1', concurrency: 40 },
async (req) => {
const uid = req.auth?.uid;
if (!uid) throw new HttpsError('unauthenticated', 'Sign in required.');
const day = new Date().toISOString().slice(0, 10); // 2026-02-14
const ref = db.doc(`usage/${uid}/daily/${day}`);
const allowed = await db.runTransaction(async (tx) => {
const snap = await tx.get(ref);
const calls = snap.get('calls') ?? 0;
if (calls >= DAILY_CALL_LIMIT) return false;
tx.set(ref, { calls: calls + 1 }, { merge: true });
return true;
});
if (!allowed) throw new HttpsError('resource-exhausted', 'Daily limit reached.');
// ...call the model with the server SDK, return the response
},
);
Set enforceAppCheck: true on the function itself. Two independent gates, both server-side.
The cost of this control is two extra document operations per generation — one read, one write — against a model call. At $0.06 per 100,000 reads and $0.18 per 100,000 writes, a million generations a month adds roughly $2.40 in Firestore charges. Compare that to a single uncapped user streaming long prompts overnight. The counter is the cheapest line item you will add this quarter.
One caveat worth naming: a single counter document per user per day caps out around one sustained write per second. That is far above any human's generation rate, but if you fan out to per-organization counters shared by hundreds of users, shard the counter or you will meet contention errors instead of rate limits.
3. Count tokens, not calls
A call limit is a weak proxy. Ten calls with 200-token prompts and ten calls with 200,000-token prompts differ by three orders of magnitude on the invoice. The response carries the numbers; store them.
const result = await model.generateContent(prompt);
const u = result.response.usageMetadata; // promptTokenCount, candidatesTokenCount, totalTokenCount
await ref.set(
{
calls: FieldValue.increment(1),
promptTokens: FieldValue.increment(u?.promptTokenCount ?? 0),
outputTokens: FieldValue.increment(u?.candidatesTokenCount ?? 0),
},
{ merge: true },
);
Once promptTokens and outputTokens are in Firestore you can price a user in a query instead of guessing from the aggregate bill. Gate on tokens, keep the call count as a crude secondary limit, and cap maxOutputTokens in your generation config so a runaway response has a hard stop. Also bound the input: truncate or reject prompts above a size you have actually priced. Unbounded context windows are a billing surface, and a user pasting a 300-page PDF is not malicious, just expensive.
4. Rules for whatever you persist
Chat features generate collections that nobody wrote rules for. Usage counters must be server-writable only, and transcripts are per-user data that a rules test should prove is per-user.
match /usage/{uid}/{document=**} {
allow read: if request.auth.uid == uid;
allow write: if false; // Admin SDK bypasses rules; clients never write here
}
match /threads/{threadId} {
allow read, delete: if request.auth.uid == resource.data.uid;
allow create: if request.auth.uid == request.resource.data.uid;
allow update: if false; // append messages as subcollection documents
}
If a client can write usage, your rate limit is decorative. Write the denial test, not just the grant — the pattern is in Testing Firestore Security Rules Before They Burn You.
5. A budget that does something
Budget alerts are email. They do not stop spend. For model APIs the quota page is the real lever: set per-minute and per-day request quotas on the AI endpoint in the Google Cloud console so a runaway costs a bounded amount while you sleep. Pair that with a Remote Config flag your function reads, so a human — or a Pub/Sub handler subscribed to the budget topic — can turn the feature off without a deploy. The wiring for that kill switch is in Hard Caps on the Firebase Bill.
The order that matters
If you ship only two of these, ship App Check enforcement and the server-side token ceiling. Attestation keeps strangers out; the ceiling keeps your own users from being expensive by accident. Everything else is refinement.
We review this exact stack as part of a Firebase audit — usually after the invoice has already made the point.
Trademark note: Firebase, Firestore, Gemini, and Google Cloud are trademarks of Google LLC. FlameRangers is an independent consultancy and is not affiliated with or endorsed by Google.