In a nutshell
A serverless API platform is the plumbing that lets a web, mobile, or partner app call your business logic over HTTPS — with someone checking IDs at the door, someone counting how often each caller knocks, and workers who only clock in when there is actual work to do (and go home, costing nothing, when it is quiet). On Google Cloud you assemble it from managed pieces: a front door that authenticates and rate-limits requests, compute that runs your code with no server to patch, a database, and one identity system that issues every badge.
Picture a well-run office building. Out front sits a global reception (the load balancer with Cloud Armor) that turns away troublemakers. Behind it are two check-in desks: a fast self-service kiosk for everyday visitors (API Gateway) and a full concierge desk for paying corporate partners who get a portal, a contract, and an itemized bill (Apigee). Both desks send visitors to the same pool of staff (Cloud Run) who appear only when there is work and leave when there is none. Records live in filing rooms (Firestore for structured records, Cloud Storage for big documents). A single badge office (Identity Platform) issues every ID — for patients, for hospital staff who bring their own corporate badge, and for machines. Cameras and a logbook (Cloud Trace, Logging, Monitoring) watch every hallway.
The whole point of “serverless” here is that you rent all of that by the request instead of renting servers by the hour: it scales to zero overnight (near-zero cost) and to thousands of concurrent requests on launch morning, with no capacity meeting and nothing to keep patched. The engineering that matters is not “can it serve HTTP” — it obviously can — but the four decisions this lesson answers: which front door, which compute, one identity for three very different audiences, and a data model and cost curve that track real usage.
Level: Advanced · Time: ~40 min
Before this lesson, it helps to know: what a container is and roughly how HTTP + JSON requests work; the idea of a JWT (a signed token that proves who a caller is); and the basics of the compute and data services this pattern assembles — the Cloud Run deep dive and a little Firestore go a long way. You do not need prior architecture experience; every term is defined in the Glossary at the end.
After this lesson you will be able to:
- Choose the right front door for a given API surface — API Gateway, Apigee, or a Global Load Balancer pointing straight at Cloud Run — and explain the cost/capability trade-off out loud.
- Pick Cloud Run vs Cloud Functions for a workload and set concurrency, min-instances, and CPU boost deliberately instead of by default.
- Wire authentication and authorization with one Identity Platform issuer, a coarse JWT check at the edge, and a fine per-tenant check at the data boundary — and know where API keys, OAuth2, and IAP each fit.
- Add rate limiting, quotas, request validation, canary releases, and tracing without hand-building any of them.
- Reason about cold starts, DR (RTO/RPO), HIPAA-grade controls, and the serverless cost curve well enough to defend the design in a review.
If your workload is an event firehose (telemetry, fan-out, sagas) rather than a request/response API, the event-driven reference architecture is the sibling pattern to read instead — this one is the governed HTTP/JSON (and gRPC) API almost every company needs first.
Every serverless API on Google Cloud eventually forces two decisions that most “hello world” tutorials skip, and getting them wrong is what turns a clean weekend prototype into an 18-month rewrite. The first is the edge: do you put API Gateway in front, or Apigee, or both — and the honest answer for a growing enterprise is both, for different audiences, which only works if you understand exactly what each one is for. The second is the compute: Cloud Functions or Cloud Run? Google has quietly merged these two until the line is blurry (Cloud Functions 2nd gen literally runs on Cloud Run), but the right default for an enterprise API is not the one most people pick. This article is the reference architecture that answers both, built on the real services in Google’s serverless stack — API Gateway and Apigee at the front door, Cloud Run (with Cloud Functions where it fits) for compute, Firestore for data, and Identity Platform for identity — and assembled into something a five-person startup and a regulated enterprise can both deploy without redrawing the diagram.
The running domain is deliberately a request/response API, not an event firehose. There is a sibling pattern for event-driven, telemetry-heavy systems; this one is about the boring, universal thing almost every company needs first: a governed HTTP/JSON (and gRPC) API that backs web, mobile, and partner clients, scales to zero when nobody is using it, scales to thousands of concurrent requests on launch day, and bills per call instead of per provisioned VM. The interesting engineering is not “can serverless serve HTTP” — obviously it can — but how to give that API one identity, a layered edge, a data model that fits a document store, and a cost curve that tracks usage, without a single server to patch.
The business scenario
Cedarline Health (fictional, used throughout) builds a patient-engagement platform — appointment booking, secure messaging, lab-result delivery, and a clinician portal — sold to mid-sized clinics and hospital groups. They are 14 engineers. Their API has four distinct consumers, and that multiplicity is the whole story:
- A patient mobile app (iOS/Android) and a patient web app — high request volume, consumer-grade sign-in (email/password, Google, Apple, SMS OTP), strict per-user data isolation.
- A clinician web portal used by staff at customer clinics — these are enterprise identities that must federate to each clinic’s own identity provider (a hospital’s Microsoft Entra ID, an Okta tenant) via SAML/OIDC, because no hospital will let staff create yet another password.
- A partner API program: lab networks, EHR vendors, and pharmacy systems that integrate machine-to-machine. These partners want a developer portal, API keys, published OpenAPI docs, usage analytics, quota tiers, and — for the labs Cedarline charges per call — monetization and billing.
- An internal/back-office surface: admin tools, batch jobs, and a couple of trusted first-party services that call the same business logic without needing the full partner-program machinery.
The traffic is spiky in two different ways at once. Within a day: quiet overnight, a booking surge 8–10 a.m. as clinics open, a lab-results wave each afternoon. Across the calendar: flu season and open-enrollment windows triple the baseline for weeks. They tried a fixed Compute Engine + managed-instance-group tier and lived the usual misery — sized for the flu-season peak and idle two-thirds of every day, or sized for the median and paged at 8:05 a.m.
The mandate from the new VP of Platform was specific and is what this architecture has to satisfy:
- One identity system spanning consumer patients and federated hospital staff and machine partners — not three bolted-together auth stacks. This is the requirement that eliminates most naive designs.
- A real partner program — a self-service developer portal, keys, quotas, analytics, and per-call monetization for the lab integrations — without standing up and running an API-management cluster by hand.
- Scale to zero overnight and absorb the flu-season ramp with no capacity meeting and no pre-warming spreadsheet.
- HIPAA-grade controls — encryption, least privilege, audit trails, data-exfiltration boundaries — because this is patient data, with a signed BAA and a security team that audits.
- A genuine DR story with a defined RTO/RPO, because a clinic that can’t pull a lab result is a clinical-safety and contract problem, not a “we’ll fix it Monday” problem.
This is the serverless sweet spot: variable, multi-audience, request-driven traffic where per-request economics beat steady-state utilization and the scarcest resource is the 14 engineers’ attention. And it scales down cleanly — a three-person startup with one clinic deploys the identical shape (API Gateway only, Cloud Run scaling to zero, single-region Firestore, Identity Platform on the free tier) for a few thousand rupees a month, and adds Apigee, multi-region, and VPC Service Controls when the partner program and the compliance auditor actually arrive. That down-scalability is what makes it a reference architecture rather than a big-company special case.
Architecture overview
The defining idea is a two-tier edge over a shared serverless core. The two front doors — API Gateway and Apigee — serve different audiences with different needs, but both terminate on the same identity, the same Cloud Run services, and the same Firestore data. Nothing about “which front door” leaks below the edge. Above it, each tier does only what it is good at.
Read the diagram top-to-bottom: a call enters through Cloud DNS and the Global Load Balancer (screened by Cloud Armor), takes one of two front doors — API Gateway for first-party apps or Apigee for the monetized partner program — then lands on a shared Cloud Run core that reads and writes Firestore and Cloud Storage; Identity Platform stands to the side as the single JWT issuer both front doors validate against, and a thin Eventarc→Cloud Run lane handles out-of-band notifications.
The request path (patient mobile app — the high-volume consumer case):
- The app authenticates the user through the Identity Platform client SDK (email/password, Google, Apple, or SMS OTP). Identity Platform returns a signed OIDC ID token / JWT carrying the user’s
sub, verified email, and any custom claims (tenant/clinic ID, role). - The app calls
https://api.cedarline.example/v1/...over TLS 1.3. DNS resolves through Cloud DNS to a Global External Application Load Balancer with Cloud Armor in front (WAF/OWASP rules, IP and geo rules, an adaptive-protection L7 DDoS layer, and a per-IP rate-based rule). - The load balancer routes the patient/first-party paths to API Gateway, a fully managed gateway purpose-built to front serverless backends. API Gateway validates the Identity Platform JWT against its issuer/JWKS, enforces per-key quotas and method-level config from the OpenAPI spec, and forwards to the backend.
- API Gateway invokes a Cloud Run service over an authenticated call (it mints an ID token for the backend’s service account; the Cloud Run service is
--no-allow-unauthenticatedand only trusts the gateway’s identity). The service runs the business logic, reads/writes Firestore scoped to the caller’s verifiedsub/tenant, and returns JSON. For a hot, idempotent GET the load balancer/CDN can cache the response.
The request path (partner lab integration — the monetized machine case):
- The lab’s server obtains an OAuth2 access token (client-credentials) and calls the Apigee endpoint (its own hostname/base path, also fronted by the global LB + Cloud Armor).
- Apigee is the full API-management plane for the partner program. On the request it runs a policy pipeline: verify the API key / OAuth token, enforce the partner’s quota and spike-arrest (rate limiting), check the request against the OpenAPI contract, capture analytics, and — for the per-call-billed lab product — record the transaction for monetization. It then routes to the same Cloud Run service the patient path uses.
- The Cloud Run service is identity-agnostic at this layer: it trusts a verified caller identity and a tenant claim handed to it by whichever edge terminated the request, and it serves the same Firestore-backed logic. The partner never sees, and never needs, the patient app’s front door — and vice versa.
The request path (clinician portal — the federated enterprise case):
- A hospital staff member signs in to the clinician web app, which uses Identity Platform’s multi-tenancy and SAML/OIDC federation: each customer clinic is a tenant, and that tenant is configured to federate to the clinic’s own IdP (Entra ID, Okta). The staff member logs in with their hospital credentials; Identity Platform brokers the federation and issues a Cedarline JWT carrying the tenant ID and role.
- From there the path is identical to the patient path — through API Gateway to Cloud Run — except the tenant claim scopes every Firestore query to that clinic’s data, and role claims gate clinician-only operations.
The data path. Firestore in Native mode is the operational source of truth: documents for patients, appointments, messages, lab results, and clinic configuration, with security rules as a second authorization layer, composite indexes for the query patterns, and transactions for idempotent writes. Large binary artifacts (lab-result PDFs, message attachments) live in Cloud Storage, referenced by object name from Firestore and served to clients via short-lived signed URLs minted by the backend. The light async touch — when a lab result is written, a patient needs a push notification — rides Firestore’s change stream via an Eventarc trigger to a tiny notification Cloud Run service, so the synchronous write path returns immediately and the notification happens out of band. (This architecture deliberately keeps the event surface small; the heavy fan-out, saga, telemetry-firehose patterns belong to the event-driven reference architecture, not here.)
The whole thing is stateless at the compute tier and regional-with-failover at the edge: every Cloud Run service is horizontally scalable and idempotent, both front doors are managed services that scale without our involvement, and the only durable state is Firestore (multi-region) and Cloud Storage (multi-region/dual-region). Drawn as a diagram it is three layers stacked: edge (Cloud DNS → Global LB + Cloud Armor → {API Gateway | Apigee}) on top; compute (a pool of Cloud Run services, fronted identically by either gateway, each running as its own least-privilege service account) in the middle; data (Firestore Native multi-region + Cloud Storage, with a thin Eventarc→Cloud Run notification side-channel) at the bottom. Identity Platform sits to the side as the single issuer every front door validates against, and Cloud Logging/Trace/Monitoring plus a VPC Service Controls perimeter wrap the whole stack.
Component breakdown
| Component | GCP service | Role here | Key configuration choices |
|---|---|---|---|
| Edge / DDoS / WAF | Global External ALB + Cloud Armor | Global anycast TLS ingress, L7 filtering, host/path routing to the two gateways | Preconfigured WAF (OWASP) rules; per-IP rate-based rules; adaptive protection for L7 DDoS; one cert/one edge in front of both gateways |
| First-party / internal edge | API Gateway | Lightweight managed gateway for patient/clinician/internal APIs | OpenAPI-defined config; JWT validation against Identity Platform issuer/JWKS; per-key quotas; authenticated invocation of Cloud Run backends |
| Partner / monetized edge | Apigee | Full API-management plane: dev portal, keys, quotas, spike-arrest, analytics, monetization | Policy pipeline (VerifyAPIKey/OAuthV2, Quota, SpikeArrest, OAS validation); developer portal + API products; rate plans for per-call billing |
| Identity | Identity Platform | One issuer for consumers + federated staff + machines | Email/Google/Apple/SMS providers; multi-tenancy (a tenant per clinic) with SAML/OIDC federation to customer IdPs; custom claims (tenant, role) set via Admin SDK; MFA |
| Compute | Cloud Run (primary) + Cloud Functions (where it fits) | Stateless business logic, request-driven, scale-to-zero | Concurrency 80 for I/O-bound handlers; min-instances only on latency-critical services; --no-allow-unauthenticated; per-service service account; CPU-boost on cold start |
| Data | Firestore (Native mode) | Operational source of truth + per-tenant document model | Multi-region (nam5/eur3) for HA; security rules as a second authz layer; composite indexes; transactions for idempotency; TTL for ephemeral docs; PITR enabled |
| Blobs | Cloud Storage | Lab PDFs, attachments, exports | Referenced by object name from Firestore; short-lived signed URLs minted by the backend; CMEK; dual/multi-region buckets; lifecycle to Nearline/Coldline |
| Light async | Eventarc (Firestore trigger) → Cloud Run | Push notification on a new lab result, out of band | Document-write trigger on results/{id}; tiny single-purpose reactor; not a general fan-out bus |
| Secrets / config | Secret Manager | Partner credentials, third-party API keys, signing material | Reached via service-account identity, never embedded in images or config; rotation; CMEK |
| Observability | Cloud Logging + Trace + Monitoring + Error Reporting | Structured logs, distributed traces, SLO alerting | Trace context propagated edge→Run→Firestore; log-based business metrics; SLO burn-rate alerts; per-channel dashboards |
| Governance / boundary | VPC Service Controls + Org Policy | Data-exfiltration perimeter and org-wide guardrails | VPC-SC perimeter around Firestore/Storage/Secret Manager; org policies (disable SA key creation, restrict regions, domain-restricted sharing) |
Four of these choices carry the design and deserve the why, because they are where this architecture diverges from a naive serverless app.
Why two front doors — API Gateway and Apigee — instead of one. The instinct is to pick one and standardize. But they sit at different points on a price/capability curve, and an enterprise API genuinely needs both points. API Gateway is a lightweight, inexpensive, fully managed gateway designed specifically to front serverless backends (Cloud Run, Functions, App Engine). It does JWT validation, API keys, quotas, and OpenAPI-driven routing — exactly what the patient, clinician, and internal surfaces need — at a fraction of Apigee’s cost and operational weight. Apigee is a full API-management platform: a developer portal, fine-grained policy pipelines, deep analytics, traffic-management primitives (spike arrest, concurrent-rate limits), API product/rate-plan modeling, and monetization — the machinery a partner program with paying lab integrations requires and that you should not hand-build. The architectural rule: API Gateway for first-party and internal APIs; Apigee for the externalized, monetized partner program. Both terminate on the same Cloud Run services, so this is two edges over one backend, not two backends. (For a team with no partner program yet, you start with API Gateway alone and add Apigee the quarter the program is funded — without touching the compute or data layers.)
Why Cloud Run is the default, and where Cloud Functions still wins. This is the decision most teams get backwards. Cloud Functions 2nd gen and Cloud Run are now built on the same substrate, but Cloud Run is the better default for an API for concrete reasons: it serves any container (so any language/runtime, any framework — Express, FastAPI, Go net/http, gRPC), supports request concurrency >1 (one instance handles 80 simultaneous requests, which slashes cost and cold-start frequency versus a function that handles one request per instance), and gives full control over the listening process. Cloud Functions earns its place for the small, single-purpose, glue pieces where you want the absolute minimum deploy unit and event wiring is the point — the Eventarc-triggered “new lab result → notify” reactor is a perfect Cloud Function (or a tiny Cloud Run service; the line is genuinely thin). The rule: Cloud Run for the API surface and anything with real business logic or non-trivial dependencies; Cloud Functions for narrow event-glue where one-function-per-concern and zero-boilerplate deploy matter more than concurrency. Defaulting your whole API to one-request-per-instance Functions is the classic cost-and-latency mistake.
Why Firestore, and what it is not good for. Firestore in Native mode is an excellent fit here because the access patterns are document- and tenant-shaped: “get this patient,” “list this patient’s appointments,” “list this clinic’s unread messages” — all single-collection, single-tenant-partition queries that return in single-digit milliseconds and scale horizontally with no sharding. It also brings two things that matter for this domain specifically: security rules (a declarative, server-enforced authorization layer that can restrict a query to documents the authenticated user owns — defense in depth behind the gateway’s coarse check), and a change stream that turns a committed write into an event via Eventarc with no outbox table. Where Firestore is the wrong tool: heavy ad-hoc relational queries, multi-entity JOINs, strong cross-entity transactional reporting, or analytics aggregations. Those are not this API’s job — but if a slice of the domain needs them, that slice sits behind the same Cloud Run/gateway/identity front door on Cloud SQL or AlloyDB instead, and the architecture absorbs it without changing shape.
Why Identity Platform rather than rolling auth or using plain Firebase Auth. The three-audience requirement (consumers + federated staff + machines) is what forces this. Identity Platform is the enterprise-grade evolution of Firebase Authentication: it adds multi-tenancy (a separate identity tenant per clinic, with isolated users and per-tenant federation config), SAML and OIDC federation to bring a hospital’s existing Entra ID/Okta in without provisioning passwords, MFA, and the SLA and support an enterprise needs — while still issuing standard OIDC JWTs that API Gateway and Apigee both validate natively. Custom claims (tenant ID, role) are stamped onto the token via the Admin SDK so the backend gets a verified tenant scope on every request rather than trusting a client-supplied ID. One issuer, validated by two front doors, covering patients, federated clinicians, and machine partners — that is the requirement that defines this architecture, and Identity Platform is the single component that satisfies it.
Implementation guidance
Provision with Terraform (the user’s house standard) in a layered state layout so blast radius is contained and teams can move independently: an edge layer (Cloud DNS, certs, the Global LB, Cloud Armor policies, the API Gateway config, the Apigee org/environment/products), an identity layer (Identity Platform config, tenants, providers, custom-claim setters), a data layer (Firestore database + indexes + security rules, Cloud Storage buckets, CMEK keys), and an app layer (Cloud Run services, service accounts, IAM bindings, the Eventarc trigger). Each layer keeps its own remote state in a GCS backend with state locking, wired together by terraform_remote_state data sources. Keep container build out of the IaC critical path: CI (Cloud Build, or GitHub Actions over Workload Identity Federation) builds and pushes images to Artifact Registry, and Terraform points Cloud Run at an immutable image digest.
Concretely:
- API Gateway from OpenAPI. The gateway config is an OpenAPI 2/3 document annotated with
x-google-backend(the Cloud Run target) andx-google-jwt-authnstyle security (the Identity Platform issuer + JWKS URI). The spec is the single source of truth: routing, JWT validation, and method config all derive from it, so the client’s generated SDK and the gateway’s enforcement can never drift. In Terraform,google_api_gateway_api→google_api_gateway_api_config(with the spec as the document body) →google_api_gateway_gateway. - Apigee as code. Model the org/environment, API proxies (the policy bundle), API products (the bundle of operations + quota a partner subscribes to), and rate plans (the monetization tiers) in Terraform / Apigee config-as-code, and publish the developer portal. Proxy policies —
VerifyAPIKeyorOAuthV2,Quota,SpikeArrest,OASValidation, and the analytics/monetization hooks — live in versioned proxy bundles deployed through CI, never click-ops’d in the console. - Cloud Run packaging. A slim container (distroless or minimal base), the listening process honoring
$PORT, concurrency 80 for I/O-bound API handlers, CPU boost on startup to cut cold-start latency,min-instances ≥ 1only on the latency-critical patient/clinician services, and startup/liveness probes. Each service runs as its own least-privilege service account.
Networking — the deliberate choices. Cloud Run, Firestore, Cloud Storage, and Secret Manager are all managed services reachable over Google’s network and governed by IAM, not by network reachability — so you do not put Cloud Run in a VPC merely to talk to Firestore. The default here is: Cloud Run ingress set to internal-and-cloud-load-balancing (so the only public path is through the Global LB → gateway, never the run.app URL directly), and the service trusting only the gateway’s invoker identity. A Serverless VPC Access connector is added only when a service must reach a private resource (a Cloud SQL instance over private IP, an on-prem system over Interconnect, or to send third-party egress through Cloud NAT for a stable, allow-listable IP — which the lab/SMS integrations need). Otherwise, isolation here is an IAM-and-resource-policy problem, not a subnet problem.
Identity wiring (the part that prevents the most incidents). One Identity Platform configuration, with a tenant per customer clinic so users, federation config, and (optionally) data partitions are isolated per customer. Consumer providers (email/Google/Apple/SMS) live on the default/patient tenant; each clinic tenant is wired to that clinic’s SAML or OIDC IdP. Custom claims (tenantId, role) are set server-side via the Admin SDK at user-provisioning time, so the backend reads a verified tenant scope from the token — it never trusts a client-supplied tenant or user ID. Authorization is two-tiered: a coarse check at the edge (API Gateway / Apigee rejects an unauthenticated, expired, or wrong-audience token before any compute runs) and a fine check at the data boundary (the Cloud Run handler — and Firestore security rules as defense in depth — constrain every query to request.auth.token.tenantId and the caller’s own documents, so a patient can read only their records and a clinician only their clinic’s). Machine partners authenticate via OAuth2 client-credentials through Apigee; their tokens carry a partner/product identity, not a patient one.
The same wiring, as code. The prose above maps to a small, concrete set of resources. These are representative, schema-correct snippets (placeholders for project IDs, image digests, and service-account emails) — not a full module, but the load-bearing pieces.
Deploy a private Cloud Run service with the concurrency, boost, and warm-floor choices from above:
gcloud run deploy patient-api \
--image=asia-south1-docker.pkg.dev/PROJECT_ID/api/patient-api@sha256:DIGEST \
--region=asia-south1 \
--no-allow-unauthenticated \
--ingress=internal-and-cloud-load-balancing \
--concurrency=80 \
--cpu-boost \
--min-instances=1 \
--max-instances=100 \
--service-account=patient-api@PROJECT_ID.iam.gserviceaccount.com
--no-allow-unauthenticated + --ingress=internal-and-cloud-load-balancing is the pair that keeps the run.app URL off the public internet; the only path in is the Global LB → gateway. Then grant only the gateway’s identity permission to call it:
gcloud run services add-iam-policy-binding patient-api \
--region=asia-south1 \
--member="serviceAccount:apigw-invoker@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.invoker"
The API Gateway config is an OpenAPI (Swagger 2.0) document — routing, JWT validation, and the backend target all derive from it:
swagger: "2.0"
info: { title: cedarline-first-party, version: "1.0.0" }
schemes: [https]
produces: [application/json]
securityDefinitions:
identityPlatform:
authorizationUrl: ""
flow: implicit
type: oauth2
x-google-issuer: "https://securetoken.google.com/PROJECT_ID"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
x-google-audiences: "PROJECT_ID"
paths:
/v1/appointments:
get:
operationId: listAppointments
security:
- identityPlatform: []
x-google-backend:
address: "https://patient-api-abc123-el.a.run.app"
jwt_audience: "https://patient-api-abc123-el.a.run.app"
responses:
"200": { description: OK }
The x-google-* extensions are what make it a gateway config rather than plain docs: the securityDefinitions block tells the gateway to validate every token’s signature against Identity Platform’s JWKS and check iss/aud, and x-google-backend names the Cloud Run target (with jwt_audience so the gateway mints the ID token the private service requires). Note API Gateway consumes OpenAPI 2.0, not 3.x.
And the Terraform that publishes it (API Gateway resources require the google-beta provider):
resource "google_api_gateway_api" "first_party" {
provider = google-beta
api_id = "cedarline-first-party"
}
resource "google_api_gateway_api_config" "v1" {
provider = google-beta
api = google_api_gateway_api.first_party.api_id
api_config_id = "v1"
openapi_documents {
document {
path = "openapi.yaml"
contents = filebase64("${path.module}/openapi.yaml")
}
}
lifecycle { create_before_destroy = true }
}
resource "google_api_gateway_gateway" "gw" {
provider = google-beta
region = "asia-south1"
api_config = google_api_gateway_api_config.v1.id
gateway_id = "cedarline-gw"
}
The second authorization layer — Firestore security rules — enforces per-tenant, per-owner isolation even if a bug in the handler forgets to:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /patients/{patientId} {
allow read: if request.auth != null
&& request.auth.token.tenantId == resource.data.tenantId
&& (request.auth.uid == patientId
|| request.auth.token.role in ['clinician', 'admin']);
}
match /appointments/{apptId} {
allow read, write: if request.auth != null
&& request.auth.token.tenantId == resource.data.tenantId;
}
}
}
request.auth.token.tenantId and role are the custom claims Identity Platform stamps server-side, so these rules trust a verified scope, never a client-supplied field.
Enterprise considerations
Security and Zero Trust. The design is Zero-Trust by construction: every request is authenticated (Identity Platform JWT or partner OAuth/key) and authorized at the edge and re-checked at the data boundary, with no implicit trust from “being inside the project” — every Cloud Run service is --no-allow-unauthenticated and grants run.invoker only to the specific gateway/Eventarc identity, so there is no anonymous east-west call. Cloud Armor filters L7 attacks and absorbs DDoS at the edge. Every service-to-data hop is least-privilege IAM — the messaging service can read/write the messages collection and nothing else; the notification service holds only the Secret Manager accessor role for the push credential. Data is encrypted at rest with CMEK (Firestore, Cloud Storage, Secret Manager) and in transit with TLS 1.2+ everywhere. Secrets live only in Secret Manager, reached via service-account identity — no keys in code, configs, or pipeline logs (this codebase has prior history with leaked DB credentials; the failure mode is designed out, not patched). A VPC Service Controls perimeter around Firestore/Storage/Secret Manager blocks data exfiltration to a project outside the boundary even if a credential leaks — the control that makes the HIPAA/BAA story credible. The biggest Zero-Trust win over a server-based design: there is no long-lived host to compromise, patch, or pivot from — compute is ephemeral and per-request.
Cost optimization. Serverless flips the model from “pay for capacity” to “pay for use,” which is exactly right for a flu-season swing. Levers, roughly in order of impact:
- Scale to zero overnight — Cloud Run, API Gateway, and the event reactors cost ~nothing idle, so the quiet 12 hours are nearly free (Firestore storage and a trickle of reads aside).
- Cloud Run concurrency, not one-request-per-instance — serving 80 requests per instance is the single biggest lever; it cuts instance-hours and cold starts dramatically versus defaulting the API to Cloud Functions.
- Right-size CPU/memory and
min-instances— hold a warm floor only on the latency-critical patient/clinician services; let everything else scale from zero. - CDN/edge cache for idempotent GETs — caching cacheable reads at the LB cuts both Cloud Run invocations and Firestore reads.
- API Gateway for the bulk, Apigee only where it pays — Apigee is materially more expensive than API Gateway; routing only the monetized partner program through it (and everything first-party through API Gateway) keeps the edge bill proportional to the value each tier provides. Apigee’s monetization, in turn, bills the labs per call, so that tier funds itself.
- Firestore read discipline — model for point reads/single-partition queries, cache hot config, and avoid fan-out reads; Firestore bills per operation.
- Budgets and alerts, watching Cloud Run billable instance time and Firestore operations as the two leading cost indicators.
Scalability. Each tier scales independently and natively — both gateways and Cloud Run are managed and elastic. The governors to set deliberately: Cloud Run max-instances per service (to protect downstreams and cap spend), concurrency tuned per handler (high for I/O-bound, low only for memory-heavy/non-thread-safe ones), and Apigee spike-arrest / quota plus API Gateway quotas at the edge. The classic serverless scaling trap is a downstream that does not scale: if a service calls a fixed-size Cloud SQL instance, Cloud Run will happily open thousands of connections and melt it — which is precisely why the hot path here is on Firestore (horizontally scalable, connectionless), and any relational dependency sits behind a connection pooler with a capped max-instances.
Reliability and DR (RTO/RPO). Within a region every component is multi-zone by default (managed services), so zonal failure is invisible. For regional DR the design uses Firestore in multi-region mode (nam5/eur3) for automatic synchronous replication across regions — RPO ≈ 0 with automatic failover handled by the service, no application change — plus dual/multi-region Cloud Storage for blobs, and infrastructure-as-code redeployable into the second region in minutes. Front-door resilience comes from the Global External ALB, which is already a global anycast service routing to the nearest healthy backend; Cloud Run services are deployed in two regions behind it, and API Gateway/Apigee front them. Because Cloud Run and the gateways are deploy-from-IaC and hold no state, the “standby region” is a genuinely warm stack rather than a cold rebuild. Targets: RPO ≈ 0 (Firestore multi-region) and RTO of minutes (health-checked LB failover + already-warm managed services in region two). Idempotency — client request IDs guarded by Firestore transactions, conditional writes — makes retries and any replay safe; Eventarc dead-lettering on the notification reactor parks a poison event instead of crash-looping.
Observability. Propagate a trace/correlation ID edge-to-data so Cloud Trace stitches “gateway → Cloud Run → Firestore” into one timeline and you can see exactly where a slow request spent its milliseconds. Cloud Run emits structured JSON logs to Cloud Logging; build log-based metrics for business KPIs (bookings/min, lab results delivered/hour) and feed Cloud Monitoring dashboards; Error Reporting groups exceptions across services. Track the serverless-specific signals: cold-start rate and duration, instance-count vs max, Firestore latency and contention, and Apigee/API Gateway 4xx/5xx and quota-rejection rates. A dashboard per channel (patient app, clinician portal, partner API, internal) keeps a problem in one surface from being masked by health in the others. Define SLOs (read p99, booking-write p99, auth success rate) and alert on burn rate, not raw error counts.
Governance. A clear resource hierarchy (org → folders for environments → a project per environment, optionally per domain), with Organization Policy constraints enforced top-down: iam.disableServiceAccountKeyCreation (no exported keys), allowed-regions, and domain-restricted sharing. Apigee is itself a governance asset — the API product catalog, versioned proxy bundles, and the developer portal make the partner contract an explicitly managed artifact, and its analytics are the per-partner audit trail. Cloud Audit Logs (Admin Activity always on; Data Access enabled on Firestore/Storage given the PHI) flow to a logs bucket / BigQuery sink for retention and SIEM. Assured Workloads can pin the whole stack to a compliance regime where required. IAM is least-privilege and reviewed; cost and ownership are attributed per service via labels.
Reference enterprise example
Cedarline Health, flu-season readiness review. Baseline (summer): ~4.2 million API operations/day. Flu-season/open-enrollment peak: ~13 million/day, concentrated 8 a.m.–6 p.m. with the sharpest spike in the 8–10 a.m. booking window. Mix: ~70% patient app/web, ~18% clinician portal, ~9% partner API (lab/EHR/pharmacy), ~3% internal/back-office.
Decisions they made and why:
- API Gateway for first-party, Apigee only for the partner program. Patient, clinician, and internal traffic — 91% of calls — go through API Gateway, which is cheap and exactly sufficient. The 9% partner traffic goes through Apigee, which gives the labs a self-service portal, keyed quota tiers, analytics, and per-call monetization. Routing only the monetized 9% through Apigee kept the edge bill proportional, and the lab rate plans made that tier revenue-positive rather than a cost.
- Cloud Run for the API, Cloud Functions for the one reactor. All API services run on Cloud Run at concurrency 80; moving off an early “everything is a Cloud Function” prototype (one request per instance) cut instance-hours roughly in half at peak and dropped p50 booking latency from ~140 ms to ~70 ms. The single Eventarc-triggered “new lab result → push notification” stayed a tiny Cloud Function because one-function-per-concern and zero-boilerplate deploy genuinely fit it.
- One Identity Platform, three audiences. Consumer providers (email/Google/Apple/SMS) on the patient tenant; a tenant per hospital customer federated via SAML to that hospital’s Entra ID/Okta so staff use their existing credentials; partners on OAuth2 client-credentials through Apigee. Custom claims (
tenantId,role) stamped server-side scope every Firestore query — a patient reads only their records, a clinician only their clinic’s, enforced both in the handler and in Firestore security rules. They never wrote a line of password-management or federation-brokering code. - Firestore multi-region, single document model. One Firestore Native database in
nam5, collections for patients/appointments/messages/results/clinic-config, composite indexes for the list queries, transactions guarding idempotent booking writes on a client request ID. Multi-region gave RPO ≈ 0 with no app changes. Lab PDFs in dual-region Cloud Storage, served by short-lived signed URLs. - DR drill. Cloud Run deployed in
asia-south1(Mumbai) andasia-southeast1(Singapore) behind the Global LB; Firestore multi-region spanning both. A GameDay — fail the Mumbai backends — saw the Global LB route to Singapore automatically and Firestore continue uninterrupted; no data lost (Firestore multi-region, RPO ≈ 0) and full request service restored in ~2 minutes as the LB health checks flipped. Measured RTO ≈ 2 min, RPO ≈ 0.
Cost outcome. The retired Compute Engine + MIG + self-managed-gateway tier had cost a flat ~₹7.0 lakh/month — sized for a flu-season peak that lasts a few weeks a year. The serverless platform billed ~₹2.1 lakh in a quiet month and ~₹6.4 lakh in a peak month, averaging ~₹3.4 lakh/month across the year — roughly a 50% reduction — while the peak was handled with no engineer paged for capacity and overnight hours cost almost nothing. The partner-API tier on Apigee, billed to the labs per call, turned the most expensive piece of the edge into a net contributor. And an entire class of work disappeared: no gateway cluster to run, no federation service to operate, no auth system to patch.
Where they spent the savings. Two engineers’ worth of reclaimed operational time went into the things serverless does not hand you free: the OpenAPI-and-Apigee-proxy contract discipline, the shared idempotency/observability library, the Firestore security-rules test suite, and the cross-region GameDay automation.
When to use it
Use this architecture when:
- Traffic is variable, spiky, or seasonal, and per-request economics beat steady-state utilization — the flu-season case.
- You serve multiple distinct audiences — consumers, federated enterprise/staff users, and machine partners — and need one identity system across all of them. This is the requirement that most clearly points here.
- You want or will want a partner API program (portal, keys, quotas, analytics, monetization) without running an API-management cluster by hand — Apigee over the same backend.
- The data model fits document / per-tenant access patterns that Firestore serves natively (most CRUD, messaging, booking, and content workloads do).
- The team is small relative to the surface area and operational attention is the binding constraint — managed services trade money for not running servers, gateways, or auth.
- You need a genuinely warm multi-region DR story with RPO ≈ 0 (Firestore multi-region) without paying for active VMs around the clock.
Trade-offs and anti-patterns to avoid:
- Defaulting the whole API to one-request-per-instance Cloud Functions. The most common cost/latency mistake on GCP serverless. Use Cloud Run with concurrency for the API surface; keep Cloud Functions for narrow event-glue.
- Forcing everything through Apigee (or refusing to adopt it). Apigee on first-party traffic burns money for capability you don’t need there; no Apigee leaves a real partner program hand-built and fragile. Use API Gateway for first-party, Apigee for the monetized partner edge — two edges, one backend.
- Putting Cloud Run in a VPC by reflex. Adds a connector and Cloud NAT bill for zero benefit when you’re only talking to Firestore/Storage/Secret Manager — those are IAM-secured. Add a connector only to reach a genuinely private resource.
- Forcing a relational, JOIN-heavy, reporting-heavy domain onto Firestore. If a slice is genuinely relational, put it on Cloud SQL/AlloyDB behind the same gateway/Cloud Run/identity front door (with a connection pooler and capped concurrency), rather than fighting the document model.
- Trusting client-supplied tenant/user IDs. Always scope queries to the verified
sub/tenantIdclaim, and back it with Firestore security rules — never the body of the request. - Ignoring cold starts on a latency-critical synchronous path. Budget
min-instances+ CPU boost for the patient/clinician hot paths; don’t discover the cold-start tail in production. - Very high, flat, predictable volume billed per-request 24/7 at full tilt. At extreme constant scale, GKE / Cloud Run committed-use behind the same Firestore/Identity-Platform core can be cheaper — measure the crossover rather than assuming serverless is always cheapest.
Alternatives worth naming: a GKE-hosted API (with the same gateway/identity/Firestore core) when you need long-lived connections, large in-memory state, sidecar-heavy service mesh, or constant high throughput; Cloud SQL/AlloyDB in place of (or beside) Firestore when the domain is relational; Cloud Endpoints as a lighter ESPv2-based alternative to API Gateway for gRPC-heavy internal services; and Firebase as the rapid-start bundle when a small team wants Identity Platform + Firestore + Functions scaffolded end to end (Firebase is much of exactly this stack with a faster on-ramp). The front-door pattern — Identity Platform identity, Global LB + Cloud Armor edge, a Cloud Run core, Firestore data — survives every one of these swaps, which is the real reason to start here: you change the compute host or the database for a slice, not the shape of the whole platform.
Going deeper
The reference shape above is the what. This section is the how it actually behaves — the internals and edge cases that separate a design that survives a launch morning from one that pages you at 8:05.
Cold starts and concurrency — the actual math
A Cloud Run instance is a running container that holds up to concurrency simultaneous requests. The autoscaler watches utilization: when the live instances can’t cover incoming load, it starts more — and a new instance means a cold start (pull the image, start the container, run your app’s init, then serve the first request). Steady-state instance count is roughly:
instances ≈ ceil( requests_per_second × avg_request_seconds ÷ concurrency )
At 400 RPS, 50 ms average latency, concurrency 80: ceil(400 × 0.05 ÷ 80) = ceil(0.25) = 1 instance. Drop concurrency to 1 (the Cloud Functions default shape) and the same traffic needs ceil(400 × 0.05 ÷ 1) = 20 instances — 20× the instance-hours and 20× the cold-start surface. That single number is why the article defaults the API to Cloud Run at concurrency 80, not one-request-per-instance Functions.
Levers to set deliberately:
concurrency— high (80–1000) for I/O-bound handlers that spend most of their time awaiting Firestore or downstreams; low (1–8) only for CPU-heavy, memory-heavy, or non-thread-safe code that would thrash if it shared an instance.min-instances— a warm floor that is never scaled to zero, so a latency-critical path never eats a cold start. You pay a reduced idle rate for these; hold them only on the patient/clinician hot paths.- Startup CPU boost (
--cpu-boost) — temporarily doubles CPU during container startup so init finishes faster; near-free and almost always worth it. cpu-idle/ “CPU always allocated” — by default Cloud Run only bills (and grants) CPU during a request (cpu_idle = true). If you run background threads, async flushes, or connection keep-alives between requests, switch to always-allocated CPU — at a higher cost.
The Cloud Run deep dive works these knobs in isolation; here the point is that concurrency is a cost and cold-start decision, not just a throughput one.
Versioning and canary — revisions and traffic splitting
Every gcloud run deploy creates an immutable revision. Cloud Run can split incoming traffic across revisions by percentage or by named tag, which gives you canary and blue-green for free — no second service, no LB surgery:
# Deploy the new revision but send it 0% of traffic, reachable only at a tag URL
gcloud run deploy patient-api \
--image=...@sha256:NEWDIGEST --region=asia-south1 \
--no-traffic --tag=canary
# Send 10% of production traffic to it
gcloud run services update-traffic patient-api \
--region=asia-south1 --to-tags=canary=10
# Happy with the metrics? Promote to 100%
gcloud run services update-traffic patient-api \
--region=asia-south1 --to-latest
The canary tag also gets its own stable URL (https://canary---patient-api-...run.app) so QA can hit the new revision directly before any live traffic touches it. Roll back by pointing traffic at the previous revision — the old container image is still there.
That is revision versioning. API versioning is separate and lives at the edge: keep breaking changes behind a path prefix (/v1, /v2) as distinct gateway configs, or — for partners — Apigee API product revisions, so a partner pinned to v1 is untouched when v2 ships. Never reuse a version’s contract for a breaking change; add a new one and deprecate on a published timeline.
What the gateway actually does with a JWT
The “validate the JWT” step is concrete: on first use the gateway fetches the issuer’s JWKS (the public keys, from x-google-jwks_uri), caches them, and for each request verifies the token’s RS256 signature, then checks iss (issuer), aud (audience — must equal your project/audience), and exp/nbf (expiry / not-before, with a little clock-skew tolerance). Only then does it forward the request, passing the decoded claims to the backend. Two things follow:
- The audience check is not optional theater — a token minted for a different project or app is a valid, correctly-signed JWT, and only the
audcheck rejects it. Gettingx-google-audienceswrong is the classic “why does every request 401 / why does a foreign token pass” bug. - The gateway does coarse authN — is this a genuine, unexpired token for us? It does not know that patient A may not read patient B’s record. That fine authZ is the handler’s job plus Firestore security rules, scoped to the
request.auth.tokenclaims. Skipping the second layer is the most common security hole in these designs.
Request and response validation — be honest about where it happens
It is tempting to assume the gateway fully validates request bodies against your OpenAPI schema. Be precise: API Gateway enforces routing, security, and path/query shape, but does not deep-validate arbitrary JSON request/response bodies against schemas. If you need strict contract enforcement at the edge, that is an Apigee job — the OASValidation policy validates a message against the OpenAPI spec — or you validate in the handler against the same schema (and cover responses with contract tests in CI). Decide this consciously; do not assume “it’s in the OpenAPI file, so the platform checks it.”
The third front door — Global LB straight to Cloud Run
API Gateway and Apigee are not the only options. You can point the Global External ALB directly at a Cloud Run service via a Serverless NEG (network endpoint group), with no gateway in between. You lose per-key quotas and edge JWT validation, but you keep Cloud Armor (WAF + rate-based rules) and gain the lowest cost and latency. It fits when auth is handled in-app or by IAP, and rate limiting can live in Cloud Armor. The decision ladder: LB → Cloud Run direct for the simplest internal or single-audience service; API Gateway when you want managed JWT/API-key/quota enforcement without running anything; Apigee when you need a developer portal, deep policy, analytics, and monetization. All three sit behind the same LB and hit the same Cloud Run core.
IAP for the internal and back-office surface
For the admin tools and first-party internal apps, Identity-Aware Proxy (IAP) is often a better fit than a hand-rolled JWT check: IAP sits at the load balancer, authenticates the user against Google identities, applies context-aware access (device, IP, and posture conditions — BeyondCorp), and passes a signed X-Goog-IAP-JWT-Assertion header that the app verifies. No login UI, no session code, no password handling — Google’s proxy does zero-trust access control for you. It is the pragmatic front door for the 3% internal surface where the users are your own staff on Google identities. See the IAP deep dive for the context-aware access model.
Rate limiting and quotas — three layers, three jobs
These are not one feature; they stack:
- Cloud Armor rate-based rules at the edge — per-IP throttling and adaptive L7 DDoS protection, applied before any compute runs. This is your blunt-instrument abuse and volumetric-attack defense.
- API Gateway quotas — per-API-key call budgets for first-party keys, metric-based.
- Apigee
Quota+SpikeArrestfor partners — and the distinction matters. Quota counts calls against an allowance over a window (10,000/day); SpikeArrest smooths the instantaneous rate (100 per second) so a partner can’t fire their whole daily allowance in one burst and knock over a downstream. You usually want both:
<SpikeArrest name="spike-arrest-partner">
<Rate>100ps</Rate>
<Identifier ref="client_id"/>
</SpikeArrest>
<Quota name="quota-partner">
<Interval>1</Interval>
<TimeUnit>day</TimeUnit>
<Allow count="10000"/>
<Identifier ref="client_id"/>
</Quota>
SpikeArrest protects your infrastructure (rate); Quota enforces the contract (volume). Reaching for one when you need the other is a common partner-program mistake — and monetized rate plans (the labs billed per call) are modeled on the same Quota/product machinery, covered in API monetization with Apigee.
VPC Service Controls with serverless — the sharp edge
VPC-SC draws an exfiltration perimeter around Firestore, Cloud Storage, and Secret Manager so a leaked credential can’t copy data to a project outside the boundary. The subtlety with managed serverless: Cloud Run, the gateways, and Eventarc are Google-managed and reach these APIs over Google’s network, so you express access with ingress/egress rules and access levels on the perimeter (which identities and sources may cross it), not with subnet firewalling. Plan for this — VPC-SC plus serverless has real configuration nuance (service coverage, ingress rules for build and deploy), and “turn on the perimeter” is not a one-click step. It is, however, the control that makes the HIPAA/BAA story credible, so it is worth the care.
The cost model, precisely
Serverless billing is per-dimension, and knowing the dimensions is how you predict the bill:
- Cloud Run bills vCPU-seconds + memory-GiB-seconds (only during requests when
cpu_idle=true) + per-request.min-instancesare billed at a reduced idle rate even with zero traffic — a warm floor is not free, just cheap. - API Gateway bills per call — cheap, and the reason 91% of Cedarline’s traffic goes here.
- Apigee carries a subscription/entitlement plus per-call cost — materially pricier, justified only where the portal/analytics/monetization pay for themselves (and where monetization bills it back to the labs).
- Firestore bills per document read / write / delete, plus storage and egress — which is why the design models for point reads and single-partition queries and caches hot config, rather than fanning out reads.
The crossover to name explicitly: at very high, flat, 24/7 volume, per-request pricing can exceed a right-sized GKE or Cloud Run committed-use footprint behind the same Firestore/Identity-Platform core. Measure the crossover with real numbers; don’t assume serverless is always cheapest at the top end.
Practice challenges
Work these in order — each builds on the last. Try before opening the solution.
1. (Beginner) Deploy a private, LB-only Cloud Run service. Deploy a service patient-api in asia-south1 that (a) rejects direct public calls, (b) is reachable only via the load balancer, © handles 80 concurrent requests per instance, (d) keeps one warm instance, and (e) runs as its own service account.
<details><summary>Solution</summary>
gcloud run deploy patient-api \
--image=asia-south1-docker.pkg.dev/PROJECT_ID/api/patient-api@sha256:DIGEST \
--region=asia-south1 \
--no-allow-unauthenticated \
--ingress=internal-and-cloud-load-balancing \
--concurrency=80 --cpu-boost \
--min-instances=1 --max-instances=100 \
--service-account=patient-api@PROJECT_ID.iam.gserviceaccount.com
Why: --no-allow-unauthenticated + --ingress=internal-and-cloud-load-balancing is the pair that removes the public run.app path; everything else is the concurrency / warm-floor / least-privilege posture from the article.
</details>
2. (Beginner) Let only the gateway invoke it. Grant the API Gateway’s service account — and nobody else — permission to call the service from challenge 1.
<details><summary>Solution</summary>
gcloud run services add-iam-policy-binding patient-api \
--region=asia-south1 \
--member="serviceAccount:apigw-invoker@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.invoker"
Why: roles/run.invoker bound to just the gateway identity is what makes “no anonymous east-west call” true — the service trusts exactly one caller.
</details>
3. (Intermediate) Make the gateway validate Identity Platform tokens. Write the OpenAPI securityDefinitions block so API Gateway validates JWTs issued by Identity Platform for project PROJECT_ID, and apply it to GET /v1/appointments.
<details><summary>Solution</summary>
securityDefinitions:
identityPlatform:
authorizationUrl: ""
flow: implicit
type: oauth2
x-google-issuer: "https://securetoken.google.com/PROJECT_ID"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
x-google-audiences: "PROJECT_ID"
paths:
/v1/appointments:
get:
security:
- identityPlatform: []
x-google-backend: { address: "https://patient-api-abc123-el.a.run.app" }
responses: { "200": { description: OK } }
Why: the gateway fetches the JWKS, verifies the RS256 signature, and checks iss/aud/exp; the x-google-audiences: PROJECT_ID line is the one that rejects a correctly-signed token minted for a different project.
</details>
4. (Intermediate) Ship a 10% canary, then promote. Roll a new image out to 10% of live traffic, verify, then take it to 100% — with a one-command rollback available.
<details><summary>Solution</summary>
gcloud run deploy patient-api --image=...@sha256:NEWDIGEST \
--region=asia-south1 --no-traffic --tag=canary
gcloud run services update-traffic patient-api \
--region=asia-south1 --to-tags=canary=10
# validate the canary tag URL + metrics, then:
gcloud run services update-traffic patient-api \
--region=asia-south1 --to-latest
# rollback if needed:
gcloud run services update-traffic patient-api \
--region=asia-south1 --to-revisions=PREVIOUS_REVISION=100
Why: revisions are immutable, so a canary is a traffic split, not a redeploy — rollback is just pointing traffic back at the prior revision. </details>
5. (Advanced) Enforce per-tenant, per-owner data isolation in Firestore. Write security rules so a patient can read only their own patients/{id} document, a clinician or admin can read any patient in their own tenant, and nobody can read across tenants.
<details><summary>Solution</summary>
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /patients/{patientId} {
allow read: if request.auth != null
&& request.auth.token.tenantId == resource.data.tenantId
&& (request.auth.uid == patientId
|| request.auth.token.role in ['clinician', 'admin']);
}
}
}
Why: the rule trusts only the custom claims (tenantId, role) that Identity Platform stamped server-side — never a client-supplied field — giving defense-in-depth behind the gateway’s coarse check.
</details>
6. (Advanced) Cap a partner at 10k calls/day and smooth bursts to 100/s. A lab partner’s contract is 10,000 calls/day, but they must not be able to fire the whole allowance in one burst. Which layer(s) do you use, and write the policy?
<details><summary>Solution</summary>
Both, in Apigee — they do different jobs:
<SpikeArrest name="spike-arrest-partner">
<Rate>100ps</Rate>
<Identifier ref="client_id"/>
</SpikeArrest>
<Quota name="quota-partner">
<Interval>1</Interval>
<TimeUnit>day</TimeUnit>
<Allow count="10000"/>
<Identifier ref="client_id"/>
</Quota>
Why: SpikeArrest bounds the instantaneous rate to protect your infrastructure; Quota counts the daily volume to enforce the contract. One without the other leaves either your downstream or your billing exposed.
</details>
Common beginner mistakes
These are conceptual traps — the wrong mental model — distinct from the architecture anti-patterns in “When to use it.”
- “Serverless means requests are always instant — no servers, no warm-up.” Wrong: a request that has to start a fresh instance eats a cold start (image pull + container start + app init). The right model: idle-to-first-request latency is real; budget
min-instances+ CPU boost on latency-critical paths and let the rest scale from zero. - “I’ll set the Cloud Run service to
--allow-unauthenticatedand just check auth in my code.” Wrong: that makes the rawrun.appURL publicly callable, bypassing the load balancer, Cloud Armor, and the gateway. The right model: keep every service--no-allow-unauthenticatedwith--ingress=internal-and-cloud-load-balancing, and grantrun.invokeronly to the gateway — the edge is the only door. - “The gateway validated the JWT, so my handler can trust the tenant/user in the request body.” Wrong: the gateway does coarse authN (is this a real, unexpired token for us?), not row-level authorization. The right model: scope every query to the verified
request.auth.tokenclaims and back it with Firestore security rules; never trust a client-suppliedtenantIdor user ID. - “Concurrency 1 is the safe default.” Wrong: one request per instance multiplies your instance-hours and cold starts (often 10–20×) for no benefit on I/O-bound handlers. The right model: default to concurrency 80, and lower it only for CPU-heavy, memory-heavy, or non-thread-safe code.
- “Route everything through Apigee so the edge is consistent.” Wrong: Apigee is materially more expensive and heavier than API Gateway; paying for it on first-party traffic burns money for capability you don’t use there. The right model: API Gateway (or LB → Cloud Run direct) for first-party, Apigee only for the monetized partner program.
- “Firestore is just SQL with a different name.” Wrong: there are no ad-hoc JOINs or cross-entity aggregations; forcing a relational, reporting-heavy model onto it fights the engine. The right model: design collections around your access patterns (point reads, single-partition lists), and put genuinely relational slices on Cloud SQL/AlloyDB behind the same front door.
Glossary
- API Gateway — a lightweight, fully managed Google Cloud gateway that fronts serverless backends; does JWT/API-key validation, quotas, and OpenAPI-driven routing at low cost. Consumes OpenAPI 2.0.
- Apigee — Google’s full API-management platform: developer portal, fine-grained policy pipelines, analytics, traffic management, and monetization. Heavier and pricier than API Gateway; for externalized/partner programs.
- Cloud Run — runs any container over HTTP(S), scales to zero, and serves multiple concurrent requests per instance. The default compute for a serverless API here.
- Cloud Functions (2nd gen) — event-glue functions that run on Cloud Run’s substrate; ideal for narrow, single-purpose reactors (one request per instance by default).
- Concurrency — the number of simultaneous requests one Cloud Run instance handles. High for I/O-bound work, low for CPU/memory-heavy work.
- Cold start — the extra latency when a request must spin up a fresh instance (image pull + container start + app init) before it can be served.
- Startup CPU boost — temporarily grants extra CPU during container startup to shorten cold-start init time (
--cpu-boost). - min-instances / max-instances — the warm floor (never scaled below) and hard ceiling (caps spend and protects downstreams) for a service’s instance count.
- Serverless NEG — a network endpoint group that lets a Global Load Balancer send traffic straight to a Cloud Run service, with no gateway in between.
- Global External ALB — Google’s global anycast Application Load Balancer; a single TLS edge that routes to the nearest healthy backend across regions.
- Cloud Armor — the edge WAF/DDoS layer: OWASP rules, IP/geo rules, per-IP rate-based rules, and adaptive L7 DDoS protection.
- Identity Platform — the enterprise evolution of Firebase Auth: multi-tenancy, SAML/OIDC federation, MFA, and standard OIDC JWTs. The single issuer here.
- JWT — JSON Web Token; a signed token carrying identity claims (
sub,aud,exp, and custom claims) that the gateway verifies. - JWKS — JSON Web Key Set; the issuer’s public keys the gateway fetches to verify a JWT’s signature.
- OIDC / OAuth2 client-credentials — OIDC is the identity layer that issues ID tokens for users; client-credentials is the OAuth2 flow machines use to get an access token with no human present.
- Custom claims — extra fields (
tenantId,role) stamped onto a token server-side via the Admin SDK, so the backend reads a verified scope rather than trusting the request body. - Multi-tenancy (Identity Platform) — a separate identity tenant per customer (clinic), each with isolated users and its own federation config.
- IAP (Identity-Aware Proxy) — a Google-managed, identity-aware proxy at the load balancer that authenticates users and applies context-aware (BeyondCorp) access, passing a signed assertion header to the app.
- Firestore (Native mode) — a serverless document database with millisecond point reads, horizontal scale, security rules, and a change stream. The operational source of truth here.
- Security rules — Firestore’s declarative, server-enforced authorization layer; restricts each query to documents the caller is allowed to see, independent of app code.
- Eventarc — the trigger service that turns an event (e.g., a Firestore document write) into a call to Cloud Run/Functions — used here for out-of-band notifications.
- Signed URL — a time-limited URL that grants direct, authenticated access to a Cloud Storage object without exposing the bucket or long-lived credentials.
- Serverless VPC Access connector — the bridge a serverless service uses to reach private resources (private-IP Cloud SQL, on-prem over Interconnect, or egress through Cloud NAT).
- VPC Service Controls — a data-exfiltration perimeter around APIs like Firestore/Storage/Secret Manager; blocks copying data to projects outside the boundary even with a valid credential.
- CMEK — Customer-Managed Encryption Keys; encryption at rest under keys you control in Cloud KMS.
- Quota vs SpikeArrest — Apigee policies: Quota counts calls over a window (volume/contract); SpikeArrest caps the instantaneous rate (infrastructure protection).
- Revision / traffic splitting — every Cloud Run deploy is an immutable revision; traffic can be split by percentage or tag across revisions for canary and blue-green releases.
- RTO / RPO — Recovery Time Objective (how fast you’re back) and Recovery Point Objective (how much data you can lose); this design targets RTO ≈ minutes, RPO ≈ 0.