Architecture diagram: GitHub Actions authenticates via Workload Identity Federation, builds and pushes a container image to Artifact Registry, and deploys it to Cloud Run. Cloud Run serves the site, whose pages fire a JS beacon on load to a Collector Cloud Function, which publishes to a Pub/Sub topic, triggering a Loader Cloud Function that writes to BigQuery, visualized in Looker Studio.

Real GCP free-tier limits, checked against this site's actual usage.

Component Free tier This site's usage Cost
Cloud Run (site) 2M requests, 180K vCPU-s, 360K GiB-s/mo — shared across Cloud Run + both Cloud Functions A few hundred visits/month, scale-to-zero $0
Collector Function 2M requests, 180K vCPU-s, 360K GiB-s/mo — shared across Cloud Run + both Cloud Functions One request per page view $0
Loader Function 2M requests, 180K vCPU-s, 360K GiB-s/mo — shared across Cloud Run + both Cloud Functions One Pub/Sub-triggered run per page view $0
Pub/Sub 10 GB message throughput/mo A few KB per event $0
BigQuery — storage 10 GB/mo A few MB of event rows $0
BigQuery — queries 1 TB scanned/mo Dashboard refreshes $0
BigQuery — streaming inserts No free tier — $0.01/200MB ~KB-scale writes ~$0.00 (rounds to zero, but not technically free)
Artifact Registry 0.5 GB storage 3 kept image versions (~150–250MB each) $0, headroom being the main thing to watch
Cloud Storage (function source) 5 GB — only in us-* regions; this bucket is in europe-west8, so not covered A few KB of zipped source, force_destroy ~$0.00
GitHub Actions 2,000 free minutes/mo One build per push $0
Looker Studio Free product Embedded dashboard $0 (queries count toward BigQuery above)

Total: effectively $0/month. The one honest caveat is BigQuery streaming inserts — billed, but at a volume too small to show up as a real number.

A few of the choices behind this architecture, and what they were weighed against.

Python end-to-end, not a JS framework

FastAPI + Jinja2 for the site, Python for both Cloud Functions. Python is my strongest language, so this keeps a solo project from context-switching, and doubles as a backend-skills demo rather than just a frontend one.

Region: europe-west8 (Milan), not europe-west1 (Belgium)

Both are free-tier equivalent at this traffic level — Milan wins on latency from home, plus a small "home turf" story for interviews.

Workload Identity Federation, not service account keys

GitHub Actions authenticates via short-lived OIDC tokens scoped to one repo (enforced by an attribute_condition on the provider) — no long-lived JSON keys sitting in GitHub secrets.

GitHub Actions + Artifact Registry, not Cloud Build

Avoids Cloud Build costs entirely while still giving a real, working CI/CD pipeline — free for public repos, and a broadly transferable skill.

Event-driven Pub/Sub pipeline, not a direct write to BigQuery

The collector function could write straight to BigQuery. It publishes to Pub/Sub instead, because the point of this project is to demonstrate the event-driven pattern itself, not just get analytics working.

Public collector endpoint: an accepted trade-off, not an oversight

A browser beacon can't authenticate, so the collector function is public. Cloud Armor rate limiting was rejected (not free-tier); routing through the FastAPI server was rejected (adds complexity for a low-value-abuse surface). Same exposure level as the site's own public Cloud Run invoker binding.

Cloud Run and Cloud Run functions collect these metrics automatically — request count, latency percentiles, container instance count — with no setup and no extra cost. No custom dashboard needed.

Cloud Run built-in metrics for the portfolio service over the last day: request count, request latencies, end-to-end latency, latency breakdown, and container instance count. Cloud Run built-in metrics for the loader Cloud Function over the last day, showing the same metric set triggered by Pub/Sub events.

This project was built in close collaboration with Claude, Anthropic's AI assistant — used deliberately, not just to generate code. Demonstrating that kind of collaboration was one of the explicit goals here, alongside the GCP architecture itself.

In practice that meant iterating on Terraform and app code with Claude while I was still learning Terraform, having it fact-check specific claims — GCP free-tier limits, icon licensing — against live sources rather than trusting recalled numbers, debugging real failures together (a container OOM crash, a CORS mismatch across Cloud Run's two default URLs, a git history conflict from an amended commit), and iterating on visual design (the architecture diagram, using official Google Cloud icons) over several rounds of feedback.

The skill on display isn't "AI wrote this" — it's directing an AI assistant on a real system: catching when a generated file path didn't match my actual repo structure, deciding which claims needed verification before publishing, and keeping ownership of every architectural decision.