Enterprise operational teams routinely pose queries whose answers are distributed across heterogeneous systems of record — CRM pipelines, ERP fulfilment data, IT ticketing queues — with no single system capable of answering them and no indexable document that captures their join. This document describes FederaQ, a federated query synthesis engine that classifies natural language query intent, decomposes queries into per-system sub-queries, dispatches them in parallel to heterogeneous enterprise connectors, enforces a freshness policy against a TTL-keyed cache, and synthesises grounded responses via LLM. The architecture is explicitly distinguished from retrieval-augmented generation (RAG): FederaQ performs no vector retrieval against an indexed corpus. Its retrieval primitive is a live API call against an operational system of record at query time, making freshness a function of source system state rather than index staleness — a load-bearing architectural distinction. All connectors are implemented as temporally mutable stateful simulators, enabling full exercise of the freshness and cache invalidation logic. API credentials are isolated behind a Google Cloud Run service, fronted by Google API Gateway with Firebase Authentication — the browser client never holds secrets, and Cloud Run itself is not publicly invocable. All design decisions are recorded as ADRs. The system runs entirely on GCP's Always Free tier at demo scale, with a hard budget cap on the underlying Gemini billing account, and is documented with a three-stage scaling graduation path.
See §03.1 latency model
Free-tier stack · §03.5
The federation gap in enterprise operational data
Between 2022 and 2025, enterprise teams broadly adopted retrieval-augmented generation as a solution to LLM hallucination. The predominant implementation pattern — documents chunked, embedded into a vector store, retrieved via cosine similarity — handles document-level static knowledge adequately. Thakur et al. demonstrated on the BEIR benchmark that dense retrievers underperform BM25 on out-of-domain corpora by 11.7 NDCG points on average,[1] a gap that widens when the query targets operational state rather than indexed text.
Operational enterprise queries — "which at-risk deals have fulfilment issues?", "how many open P1 tickets do we have right now?" — require current state from systems of record, not historical text from indexed documents. The answer to such a query does not exist as a document. It exists as a join across live API endpoints that were never designed to communicate with each other. Dresner Advisory Services estimates that enterprise analysts spend 30–45 minutes per day context-switching across systems to manually assemble answers that cross system boundaries.[2]
Three developments make a systematic federated synthesis approach tractable as of 2026. First, Schick et al.'s Toolformer demonstrated that LLMs can reliably self-direct API invocations from natural language input with minimal fine-tuning,[3] establishing the theoretical basis for LLM-directed connector routing. Second, Gemini's two-tier model family — a lightweight Flash-Lite tier for high-volume routing decisions and a stronger Flash tier for reasoning-visible synthesis — makes cost-appropriate multi-step LLM orchestration viable against the Gemini Developer API at negligible per-query cost.[4] Third, Promise.all() enables parallel async fan-out to multiple connectors with latency bounded by the slowest, not the sum, regardless of whether the fan-out runs in the browser or, as this revision requires, inside a Cloud Run service.
What document-indexed retrieval cannot answer
The failure mode of document RAG on operational enterprise queries is not noisy — it is silent. A document retrieval system returns an answer grounded in indexed text. When the indexed text is a policy guide or a sales playbook, the retrieval produces a fluent, confident response to a question that required live operational data. The document was the wrong unit of truth.
Guu et al. established that open-domain QA systems relying on static corpora degrade predictably when the query targets time-sensitive entity state — a finding that extends directly to enterprise operational queries where the correct answer is a function of current system state, not historical documentation.[5] FederaQ addresses this class of query exclusively; static document lookup is explicitly out of scope.
2.1 — Query type taxonomy and failure modes
| Query Type | Example | Document RAG | FederaQ |
|---|---|---|---|
| Cross-system synthesis multi-source | "At-risk deals with fulfilment issues this quarter?" | Fails — no join exists | Core use case |
| Live operational status point-in-time | "How many open P1 tickets do we have right now?" | Stale answer | Live connector |
| Inventory & fulfilment ERP-grounded | "Which SKUs are below reorder threshold today?" | No document exists | ERP connector |
| Pipeline health CRM-grounded | "How is Q2 tracking against target by stage?" | Stale if indexed | CRM connector |
| Document lookup static knowledge | "What is our refund policy?" | Handles correctly | Out of scope |
Six layers. One query. Grounded synthesis.
FederaQ is structured as six discrete layers. Each layer has a single responsibility. The boundary between layers is a well-defined interface. Layer 2 (Proxy) is a new architectural addition that resolves the credential isolation requirement: the browser client never holds API keys. The proxy layer is now realised as a Cloud Run service, fronted by Google API Gateway, which validates a Firebase-issued Google Sign-In JWT before any request reaches Cloud Run — Cloud Run itself grants run.invoker only to the Gateway's service account, never to allUsers. All upstream calls to Gemini and, in production, to enterprise connectors, are mediated through this layer.
3.1 — End-to-end latency model
The following latency budget is derived from documented component benchmarks. Gemini Flash-Lite and Flash token-generation figures are from Google's published Gemini Developer API benchmarks.[4] API Gateway adds a JWT-validation hop in front of Cloud Run; Cloud Run itself scales from zero on a cold start of roughly 1–2s for a small Node container, amortised to near-zero on warm invocations within the same session.[6] All figures are P50 estimates; P99 analysis follows.
3.2 — Connector federation model
3.3 — Adaptive output format selection
3.4 — Technology stack
| Component | Choice | Alternative | Rationale |
|---|---|---|---|
| LLM inference | Gemini 3.1 Flash-Lite + Gemini 3.5 Flash | Vertex AI Gemini | AI Studio Developer API key, no IAM setup; Vertex AI is the documented production upgrade for SLA + VPC-SC [4] |
| Credential proxy | Cloud Run + Google API Gateway | Cloud Functions (2nd gen) | Scale-to-zero, Always Free 2M req/mo; API Gateway validates Firebase JWTs before Cloud Run is reachable [6] |
| Auth | Firebase Authentication · Google Sign-In | Identity Platform | Zero-config with Firebase Hosting; sufficient for single-tenant portfolio scope |
| Intent classification | Gemini JSON-schema response | Fine-tuned BERT | Native responseSchema constrains output structurally, not just by prompt instruction; deterministic at temp=0.0 |
| Query planner | Rule-based template | Second LLM call | Removes ~180ms from critical path; fully deterministic and testable |
| Connectors | Stateful mutable simulators | Live Salesforce/SAP/Jira | Exercises TTL cache under real conditions; real-API-swappable interface |
| Cache layer | Firestore Native mode + TTL | Memorystore (Redis) | Memorystore has no free tier at all; Firestore's Always Free daily quota (50k reads/20k writes) covers demo traffic at $0 |
| Secrets | Secret Manager | Cloud Run env var (plaintext) | Gemini API key injected at deploy time; never in source control or client code |
| Frontend | Static HTML + vanilla JS | React / Next.js | Zero build step; Firebase Hosting compatible |
| Hosting | Firebase Hosting (Spark) | Cloud Storage + CDN | Genuinely free — no billing account required for this layer; global CDN included |
Credential isolation via Cloud Run + API Gateway + Firebase Auth
4.1 — Cloud Run allowlist check
API Gateway validates the Firebase-issued JWT before a request ever reaches Cloud Run — that validation is declared in the Gateway's OpenAPI spec, not application code. Cloud Run still applies its own authorisation check on the forwarded identity, matching the least-privilege pattern used in the reference builds:
// server.ts · Cloud Run · federaq-service // Gemini key comes from Secret Manager via env var — never in source, never in client code function requireAllowlist(req, res, next) { // API Gateway already validated the Firebase JWT; it forwards the // decoded claims in this header. Cloud Run trusts Gateway, not the caller. const userInfoHeader = req.headers['x-apigateway-api-userinfo']; if (!userInfoHeader) { return res.status(403).json({ error: 'Missing identity header' }); } const claims = JSON.parse(Buffer.from(userInfoHeader, 'base64url').toString()); const allowlist = process.env.ALLOWED_EMAILS.split(','); if (!allowlist.includes(claims.email)) { return res.status(403).json({ error: 'Not authorized: ' + claims.email }); } next(); } app.post('/query', requireAllowlist, async (req, res) => { // Gemini call happens only after both checks pass — Gateway's JWT // validation, then this service's own allowlist const classification = await classifyIntent(req.body.query); // ... connector fetch, cache, synthesis — unchanged from §03 });
TTL policy with stateful connector mutation
5.1 — TTL value derivation
TTL values are not arbitrary. Each value is derived from documented source-system update frequency characteristics and operational staleness tolerance for the query type. The following derivations apply to the design phase; production values would be empirically calibrated against actual connector event logs.
| Query Type | TTL | Derivation basis | Source |
|---|---|---|---|
| Inventory | 60s |
SAP MM standard batch posting cycle in SMB deployments: 60–300s. 60s represents the aggressive polling end prior to webhook integration. | [7] |
| Live tickets | 90s |
Jira webhook delivery SLA documented at up to 60s latency. 90s provides a safety margin over the documented delivery bound. | [8] |
| Deal stage | 5min |
CRM deal stage updates are human-initiated; Salesforce Streaming API push latency is 1–3s but human update frequency is measured in minutes to hours. 5min staleness is operationally acceptable for pipeline review queries. | [9] |
| Account info | 30min |
Account metadata (owner, segment, contract tier) changes infrequently. Salesforce account record audit logs show median update intervals of 2–4 hours in SMB accounts. | [9] |
5.2 — Cache routing decision tree
5.3 — Stateful connector mutation model
// Stateful connector — ERP inventory simulator // State mutates on a deterministic schedule, exercising real TTL events class ERPConnector { constructor() { // Seed from load timestamp — deterministic, reproducible this.seed = Date.now(); this.state = this.initState(); // Mutate every 15 seconds — faster than 60s TTL, guarantees TTL miss events setInterval(() => this.mutate(), 15000); } mutate() { // Deterministic delta: decrement SKU quantities, flip reorder flags this.state.skus = this.state.skus.map(sku => ({ ...sku, quantity: Math.max(0, sku.quantity - Math.floor((Date.now() % 7))), below_reorder: sku.quantity < sku.reorder_threshold })); this.state.last_mutated = Date.now(); } query(intent, filters) { return { data: this.state, source: 'ERP · SAP', fetched_at: Date.now(), // stamped at query time, not mutation time ttl_policy: 60 // seconds }; } }
Eventual consistency with explicit staleness signalling
The system queries three connectors via Promise.all() at a point in time. Each connector returns its own fetched_at timestamp. When connector results are temporally inconsistent — for example, when ERP inventory state reflects a cache hit from 55 seconds ago while the CRM deal stage is live — the synthesis may join data from different operational moments. In enterprise operational contexts, this can produce materially incorrect conclusions for time-sensitive decisions.
The consistency model below defines the system's formal consistency posture, the consistency window threshold, and the handling rule for skew violations.
Intent classifier, rule-based planner, and failure taxonomy
7.1 — Classifier prompt (full specification)
responseSchema constrains the output at the decoding level, not just by instruction — the model literally cannot emit a field outside the declared enum. The classification taxonomy below is unchanged; only the enforcement mechanism is stronger.
// gemini/classify.ts · Cloud Run · gemini-3.1-flash-lite · temp=0.0 const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); // ← Secret Manager, never client code // Classification taxonomy — unchanged from the design phase: // intent: "cross-system" | "live-status" | "inventory" | "pipeline" | "single-connector" // connectors: ["CRM"] | ["ERP"] | ["Ticketing"] | any combination // format: "TABLE" | "CHART" | "CARD" | "PROSE" // sensitivity: "high" | "low" const response = await ai.models.generateContent({ model: 'gemini-3.1-flash-lite', contents: `${CLASSIFIER_GUIDANCE}\n\nQuery: "${query}"`, config: { temperature: 0, responseMimeType: 'application/json', responseSchema: { // ← decoding-level constraint, not just instruction type: Type.OBJECT, properties: { connectors: { type: Type.ARRAY, items: { type: Type.STRING, enum: ['CRM','ERP','Ticketing'] } }, format: { type: Type.STRING, enum: ['TABLE','CHART','CARD','PROSE'] }, sensitivity: { type: Type.STRING, enum: ['high','low'] }, }, required: ['connectors', 'format', 'sensitivity'], }, }, }); // Confidence and format-confidence fallback rules (§7.2, F2/F3) are unchanged — // they still govern what happens when the classifier is uncertain, not whether it is.
7.2 — Classifier failure taxonomy
| Failure case | Detection | Handling path |
|---|---|---|
| F1 · Malformed JSON response | JSON.parse() throws |
Retry once at temp=0.0. If retry fails, default to full connector fan-out (all three) and PROSE format. Log miss. |
| F2 · Confidence below 0.60 | result.confidence < 0.60 |
Activate all three connectors. Conservative fallback — retrieval overhead is preferable to a silent routing miss. Log confidence value. |
| F3 · Format signal absent or low confidence | result.format_confidence < 0.70 or field absent |
Apply deterministic rule: multi-connector → TABLE; single-connector scalar → PROSE. No additional LLM call required. |
| F4 · Connector set returned empty | result.connectors.length === 0 |
Surface explicit "cannot determine routing" error to UI. Do not produce a synthesis. Log the query for review. |
| F5 · Connector timeout (>3s) | Promise.allSettled() timeout |
Proceed with partial results from connectors that responded. Surface timeout warning per connector in source attribution. Log SLA miss. |
7.3 — Rule-based query planner
The Query Planner takes the classifier output and generates per-connector sub-queries using a deterministic template, not a second LLM call. This removes ~180ms from the critical path and makes the planner fully unit-testable.
Architecture ceiling and three-stage graduation path
Cloud Run's scale-to-zero, Firestore's Always Free daily quota, and the Gemini Developer API's free/low-cost tier constitute a single-tenant, budget-capped architecture. At demo scale every component sits inside a perpetual free quota; the binding constraint is not user count but the deliberately tiny $0.01 hard cap on the Gemini billing account, which is a policy choice, not a technical ceiling. The following table defines what changes at each tier and, critically, which change is a technical requirement versus a budget policy that could be lifted without any architectural change.
| Tier | Concurrent users | Bottleneck | Cache | LLM inference | Connector layer |
|---|---|---|---|---|---|
| Demo current |
1 (allowlisted) | $0.01 Gemini budget cap — policy, not quota | Firestore Native mode, Always Free daily quota | Gemini Developer API, AI Studio key | Stateful simulators |
| Team-scale Stage 2 |
10–50 | Firestore Always Free read/write quota; Cloud Run concurrency | Firestore, raised budget alert instead of hard cap | Gemini Developer API, raised budget | Simulators + 1 live connector (Salesforce Dev Ed.) |
| Production Stage 3 |
500+ | Connector connection pooling; LLM token throughput | Memorystore (Redis) — no free tier, billed from first byte | Vertex AI Gemini — SLA, quota management, VPC-SC | Live Salesforce / SAP / Jira via Apigee + OAuth |
ADM Phase B + Phase C positioning
This document operates as a Phase B + Phase C artifact within the TOGAF Architecture Development Method. Phase B establishes the business context — the federation gap, the query taxonomy, and the stakeholder concern (Sales Directors, Ops leads) whose cross-system queries this system addresses. Phase C defines the information systems architecture — the connector federation model, the data flow between layers, and the freshness policy as a data architecture constraint.
9.1 — Architecture Building Blocks
| ABB | Description | Realised by (SBB) |
|---|---|---|
| Connector Federation ABB | The capability to dispatch parallel sub-queries to heterogeneous operational data sources via a standard interface | Layer 4 · CRM/ERP/Ticketing connectors with query(intent, filters) interface |
| Query Intelligence ABB | The capability to classify natural language query intent and generate per-connector sub-queries | Layer 3 · Gemini 3.1 Flash-Lite classifier (AI Studio) + rule-based planner |
| Credential Isolation ABB | The capability to mediate all upstream API calls without exposing secrets to the client | Layer 2 · Cloud Run + API Gateway + Firebase Authentication |
| Freshness Policy ABB | The capability to enforce per-query-type TTL policies and surface data age to the user | Layer 5 · Firestore TTL cache with consistency window check |
9.2 — Phase A (Architecture Vision) mapping
The Architecture Vision is captured in §01 (Motivation): enterprise operational teams are blocked from answering cross-system queries because no single system of record contains the required join, and no existing tooling automates the federation. FederaQ addresses this gap without requiring application migration, new enterprise software licenses, or changes to existing systems of record — consistent with the TOGAF ADM principle of maximising reuse of existing infrastructure.[10]
All significant architectural choices · traceable rationale
- Context
- The original system was titled a "RAG system." RAG requires vector retrieval from an indexed corpus. FederaQ performs live API federation against operational systems of record. The distinction is architecturally load-bearing: freshness is a function of source system state, not index staleness.
- Decision
- Classify FederaQ as a federated query synthesis engine. The closest published patterns are Toolformer [3] for LLM-directed API invocation, Trino/Presto [11] for federated query execution, and DAIL-SQL for structured sub-query generation. All RAG references are removed except where document RAG is being contrasted as the inadequate prior approach.
- Consequences
- The system no longer claims RAG capabilities it does not implement. The architecture is correctly classified and comparable to published prior work. Any future extension to include vector retrieval over static documents would require a new connector type and a revised problem statement.
- Alternatives
- Retain "RAG" label with a redefined definition — rejected as misleading to any evaluator familiar with the retrieval-augmented generation literature.
- Context
- The original ADR-002 accepted the Groq API key in browser client code as "acceptable for a portfolio demo." This was an acknowledged vulnerability with no resolution path — not an architecture decision. A TOGAF-compliant security architecture must address credential isolation regardless of demo scope.
- Decision
- Introduce a Cloudflare Worker as a mandatory proxy layer between the browser client and all upstream API calls. The API key is stored as a Cloudflare Worker environment secret (via Wrangler), never deployed to client code. The Worker enforces CORS, validates request origin, and applies rate limiting via Cloudflare KV. The browser client calls the Worker endpoint only.
- Consequences
- API key is never exposed. Cold-start latency is zero (Cloudflare Workers use V8 isolates, not containers). Free tier covers 100,000 requests/day — sufficient for demo and team-scale use. CORS surface is controlled at the Worker, not the LLM provider.
- Alternatives
- FastAPI proxy on Railway free tier — adds 500ms–2s cold-start latency and operational complexity. Rejected. Netlify Functions — viable alternative but requires a build step. Cloudflare Workers preferred for zero cold-start and edge deployment.
- Superseded by
- ADR-009. This decision is retained verbatim as the historical record — it was the correct call for a non-GCP-hosted design. The GCP-native migration replaces the platform, not the reasoning: both decisions solve the same problem the same way in spirit — mandatory server-side mediation, zero client-held secrets.
- Context
- The original design used static JSON objects as mock connectors. Static connectors cannot exercise the TTL cache layer — no state change means no TTL miss, no cache invalidation event, and no demonstration that the freshness policy produces different results on a repeated query. The freshness policy section was therefore unvalidated design.
- Decision
- Replace static JSON mocks with stateful mutable simulators. Each connector seeds its initial state at page load and applies a deterministic delta function on a fixed interval (CRM: 20s, ERP: 15s, Ticketing: 30s). All intervals are shorter than the corresponding TTL values, guaranteeing real TTL miss events during a demo session. The connector interface is unchanged:
query(intent, filters) → {data, source, fetched_at, ttl_policy}. - Consequences
- The freshness policy is exercised under real conditions. The simulator can demonstrate a TTL miss producing a changed value. The connector interface remains real-API-swappable. State is not persistent across page reloads — acceptable for demo scope.
- Alternatives
- Salesforce Developer Edition for CRM — free but covers only one connector. ERP and Ticketing have no equivalent free sandbox. Deferred to Stage 2 scaling.
- Context
- The Query Planner generates per-connector sub-queries from classifier output. Two implementation options exist: a second LLM call (a Groq call in the original design, a Gemini call in the GCP-native rebuild) or a rule-based template engine over the classifier's structured output.
- Decision
- Implement the Query Planner as a deterministic rule-based template. Given classifier output
{intent, connectors, signals}, a static template map generates the per-connector sub-query payload:template[intent][connector] → {fields, filters, limit}. This removes ~180ms from the critical path (one fewer model call, regardless of provider), makes the planner fully unit-testable, and eliminates a second point of LLM non-determinism. - Consequences
- The planner handles only the query types defined in the template map. Novel query types not in the map require a template addition. For the current four-connector, five-intent taxonomy, this is a manageable surface. A second LLM call would be required if query decomposition complexity grows beyond what templates can express.
- Alternatives
- Second Groq LLM call — adds ~180ms latency, introduces non-determinism, requires prompt maintenance. Rejected for the current query taxonomy.
- Context
- Format selection must handle the case where the LLM format signal is absent or low-confidence. The original design fell back to PROSE for all unresolved cases. A ranked table of 40 deals rendered as prose is operationally unusable — the document itself acknowledged this while retaining the contradiction.
- Decision
- Introduce a format confidence threshold (0.70). If
format_confidence ≥ 0.70, use the LLM-supplied format signal. If below threshold or absent, apply a deterministic rule: multi-connector queries default to TABLE; single-connector scalar queries default to PROSE. This is consistent with the primary use case and requires no additional LLM call. - Consequences
- Format fallback is now consistent with usability requirements for cross-system queries. The deterministic rule is auditable and testable. Edge cases remain possible where the deterministic rule selects a suboptimal format, but these are preferably wrong rather than unusably wrong.
- Alternatives
- Always PROSE — rejected; unusable for cross-system results. Always TABLE — rejected; produces poor output for single-scalar queries and prose explanations.
- Context
- Cache layer is needed to demonstrate the freshness policy without hitting connectors on every query. The original design used a browser-side in-memory Map with TTL — which cannot survive the move to a server-side Cloud Run proxy, since Cloud Run instances are stateless and scale to zero between requests; an in-memory cache would evaporate on every cold start. A real persistent store is now a hard requirement, not an optimisation.
- Decision
- Use Firestore Native mode (
europe-west2) as the TTL cache, keyed by a hash of the query text, with expiry checked in application code at read time rather than relying on Firestore's own TTL policy deletion (which runs on a delayed background schedule, not query-time). Memorystore — Google's managed Redis-compatible service — was the more obvious fit for a TTL cache, but it has no free tier at all; it is billed from the first byte, which conflicts directly with the $0-target constraint. Firestore's Always Free daily quota (50,000 reads / 20,000 writes / 20,000 deletes) covers demo-scale traffic at $0. The consistency window check is unchanged: computemax(fetched_at) - min(fetched_at)across all connector results in a synthesis call; if delta > 120s, append a staleness warning to the source attribution panel. - Consequences
- No automatic expiry enforcement (checked at read time, not Firestore-enforced) and no distributed invalidation across concurrent Cloud Run instances — acceptable at single-service, allowlisted-user scale. High-sensitivity query types still bypass the cache entirely regardless of backing store. Memorystore remains the documented production upgrade path once traffic or invalidation complexity exceeds what a simple read-time TTL check handles well (§08, Stage 3).
- Alternatives
- Memorystore (Redis) — rejected for demo scope, no free tier. Cloud Run in-memory cache — rejected, does not survive scale-to-zero cold starts. Cloudflare KV — no longer applicable once the proxy platform moved to GCP (ADR-009).
- Context
- Portfolio artifact must be hostable on GitHub Pages with zero build infrastructure and zero cost.
- Decision
- Single
index.html— all CSS, JS, SVG diagrams, simulator logic, and ADRs self-contained. No bundler, no framework, no build step. - Consequences
- File grows large. No component separation. Acceptable for a portfolio artifact — prioritises deliverability over maintainability. Simulator interactivity achieved via vanilla JS without framework overhead.
- Alternatives
- React + Vite on Vercel — unnecessary complexity for a static portfolio document.
- Context
- Not all enterprise queries require live data. The original TTL values (inventory 60s, tickets 90s, deal stage 5min, account info 30min) were asserted without derivation. This revision provides a documented derivation basis for each value.
- Decision
- Retain the TTL policy map. Add derivation rationale per query type (see §05.1). Values are design-phase estimates; production values would be empirically calibrated against actual source-system event log update frequencies. High-sensitivity types (inventory, live tickets) bypass cache regardless of TTL status.
- Consequences
- Staleness risk on low-sensitivity queries remains. Policy map must be maintained as query types evolve. Data age is surfaced to UI on every response, mitigating user trust risk. Consistency window check (ADR-006) provides an additional signal for temporally skewed results.
- Alternatives
- Always-live (no cache) — correct but expensive and high-latency at scale. Always-cached — cheap but operationally unacceptable for freshness-sensitive queries.
- Context
- The GCP-native rebuild retires the Cloudflare Worker (ADR-002) — not because it was wrong, but because the target platform changed. The available GCP billing account has no balance; every resource choice has to sit inside an Always Free perpetual quota, and the account must not be reachable by an anonymous caller who could run up the connected Gemini billing account.
- Decision
- Front Cloud Run with Google API Gateway. Firebase Authentication (Google Sign-In only) issues an ID token; API Gateway validates that token against Firebase's issuer (
securetoken.google.com/<project>) before a request is ever forwarded, and passes the decoded claims to Cloud Run via thex-apigateway-api-userinfoheader. Cloud Run grantsroles/run.invokeronly to the Gateway's own service account — neverallUsers— so Cloud Run is not directly reachable from the internet at all. Cloud Run additionally checks the forwarded email against an allowlist before doing any Gemini call. - Consequences
- Two authorisation checks instead of one (Gateway's JWT validation, then Cloud Run's allowlist) — more moving infrastructure than a single Cloud Run service, but it is the least-privilege pattern: no caller reaches billable compute without a verified Google identity, and no caller reaches the Gemini key without also being on the allowlist. All four pieces (Cloud Run, API Gateway, Firebase Auth, Firestore) require a Blaze billing account attached to the project even though usage is designed to stay inside Always Free quotas — that requirement is unavoidable on GCP and is addressed directly as a budget-governance decision in ADR-011, not worked around here.
- Alternatives
- Cloud Run with no Gateway, validating the Firebase JWT in application code directly — rejected; it works, but puts token-validation logic in the same codebase as business logic instead of a declarative, auditable OpenAPI security definition. Firebase Hosting + Cloud Functions (Spark plan) — rejected; Spark-tier Cloud Functions cannot make outbound calls to non-Google APIs reliably enough for a proxy that must reach the Gemini Developer API, and Blaze is required for Cloud Functions regardless once real usage is expected.
- Context
- Groq is not a GCP-native service. Moving the proxy to Cloud Run is the natural point to also move inference to a Google-native LLM. Classification and synthesis have different cost/quality requirements — classification runs on every query and needs to stay cheap; synthesis is where reasoning quality is user-visible.
- Decision
- Route classification to
gemini-3.1-flash-liteand synthesis togemini-3.5-flash, both called from Cloud Run via the@google/genaiSDK against the AI Studio Developer API (an API key, not Vertex AI's IAM-based auth). The key lives in Secret Manager, injected into Cloud Run as an env var at deploy time. Gemini's nativeresponseSchemareplaces the prompt-instruction JSON parsing the Groq classifier relied on (§7.1) — the model is constrained at the decoding level rather than by instruction alone. - Consequences
- Two model calls to reason about instead of one, but each stage runs the right-sized model for its job, matching the original Groq design's single-model-two-jobs tradeoff analysis, just with a stronger structured-output guarantee. Vertex AI remains the documented production upgrade (§08, Stage 3) for SLA guarantees, quota management, and regional data residency — deliberately not adopted now, since Vertex AI's IAM-based billing model doesn't map cleanly onto the AI Studio billing account this build is scoped to use.
- Alternatives
- Vertex AI Gemini now instead of at production graduation — rejected for this phase; it requires project-level IAM setup disproportionate to a $0.01-capped demo, and the user's existing billing relationship is with AI Studio specifically. Keeping Groq behind the new Cloud Run proxy — rejected; defeats the purpose of a GCP-native rebuild and adds a second external vendor dependency for no benefit.
- Context
- The Gemini API key used by this project bills against an AI Studio billing account that is shared with other, unrelated projects and carries a fixed $10 balance. A Cloud Billing Budget alert on its own only sends a notification — it does not stop spend — and $0.01 leaves no margin for a stray retry loop or an unexpectedly large classification/synthesis batch to eat into balance meant for other work.
- Decision
- Scope a Cloud Billing Budget to just this project's Generative Language API SKU, set at $0.01, wired to a Pub/Sub notification that triggers a Cloud Function to disable the Gemini API key immediately on breach — not merely email a warning. This is layered on top of, not instead of, keeping demo traffic on Gemini's free-tier rate limits wherever the model choice allows it, so the paid path is a safety margin rather than the expected mode of operation.
- Consequences
- A false-positive disable (e.g. a legitimate short burst of demo traffic) locks out the key until manually re-enabled — an intentional tradeoff, since the alternative is risking spend against a balance shared with other projects. This budget is separate from, and does not affect, the main GCP project's own $0 budget alert on Cloud Run/Firestore/API Gateway usage (§08), which sits on different billing infrastructure entirely.
- Alternatives
- Budget alert only, no auto-disable action — rejected; an alert that arrives after the spend has already happened doesn't cap anything. Separate, dedicated billing account for this project alone — preferable in principle, but not available given the user's current $10 AI Studio account is the one with an active balance.
Architectural parameter simulator · TTL and latency instrumentation
NL query
intent
sub-queries
fetch
check
Gemini
output
Citations
- [1]Thakur, N. et al. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS 2021 Datasets and Benchmarks Track. arXiv:2104.08663.
- [2]Dresner Advisory Services. (2024). Wisdom of Crowds® Business Intelligence Market Study. Dresner Advisory Services LLC. [Analyst report; context-switching productivity data cited from executive survey findings.]
- [3]Schick, T. et al. (2023). Toolformer: Language Models Can Teach Themselves to Use Tools. NeurIPS 2023. arXiv:2302.04761.
- [4]Google LLC. (2026). Gemini API Documentation — Models, Rate Limits, and AI Studio Developer API. ai.google.dev/gemini-api/docs. [Gemini 3.1 Flash-Lite and Gemini 3.5 Flash; Developer API key auth via AI Studio; free-tier and pay-as-you-go pricing.]
- [5]Guu, K. et al. (2020). REALM: Retrieval-Augmented Language Model Pre-Training. ICML 2020. arXiv:2002.08909.
- [6]Google LLC. (2026). Cloud Run, API Gateway, and Firebase Authentication Documentation — Always Free Tier Limits and Scale-to-Zero Behaviour. cloud.google.com/run/docs, cloud.google.com/api-gateway/docs, firebase.google.com/docs/auth. [Cloud Run Always Free: 2M requests/month; scale-to-zero with cold start on first request; API Gateway JWT validation against Firebase issuer.]
- [7]SAP SE. (2023). SAP Materials Management (MM) Configuration Guide — Batch Job Scheduling for Inventory Posting. help.sap.com. [Standard batch posting cycle documented at 60–300s for SMB deployments.]
- [8]Atlassian Inc. (2024). Jira Cloud REST API Documentation — Webhooks: Delivery SLA and Retry Policy. developer.atlassian.com/cloud/jira. [Webhook delivery latency documented up to 60s; retry on failure.]
- [9]Salesforce Inc. (2024). Salesforce Streaming API Developer Guide — Push Topics and Event Delivery Latency. developer.salesforce.com. [CRM Streaming API push latency 1–3s; deal stage human-update frequency empirically measured in minutes to hours.]
- [10]The Open Group. (2018). TOGAF® Standard, Version 9.2. The Open Group. ISBN: 978-94-018-0-303-6. [ADM principles: maximise reuse, Phase B Business Architecture, Phase C Information Systems Architecture.]
- [11]Sethi, R. et al. (2019). Presto: SQL on Everything. IEEE ICDE 2019. [Federated query execution model; connector interface design principles.]