Overview

Two full deployments of the same product

Client
Frontend
Next.js chat UI
Recruiters ask questions about background, skills, and projects directly
POST /chat · GET /conversation
Two Clouds
GCP + Vercel LIVE NOW
twin-gcp
Next.js on Vercel → FastAPI on Google Cloud RunGoogle Gemini (gemini-flash-latest) for generation, Firestore for conversation history. Gemini key read from Secret Manager at Cloud Run startup. GitHub Actions deploys via Workload Identity Federation — no long-lived credentials.
AWS BUILT, NOT LIVE
digital-twin
Statically-exported Next.js behind CloudFront + S3 → FastAPI adapted to Lambda via mangum, behind API GatewayBedrock for generation, S3 for conversation memory. 3 Terraform workspaces (dev/test/prod), remote state in S3 + DynamoDB lock. Fully documented, deliberately not paid for indefinitely.
both versions: typed config, retry-with-backoff, mocked-client test suite
Backend Design
Deliberate Monolith
One FastAPI service
Typed env-driven config (pydantic-settings) · typed exception hierarchy → uniform JSON error shape · route logic separate from app wiring
Reliability
Retry-with-backoff on the LLM call
3 attempts, exponential backoff, only on 429/500/503 · non-transient errors fail fast · 3 tests cover exactly these paths
Decisions

Why two deployments, and every choice inside each

DecisionWhat the naive approach gets wrongWhy this solution is better
Two clouds, not one migration Moving the AWS version to GCP and calling it done tells a weaker story — "I moved my app" isn't an engineering demonstration Two live, working, differently-architected deployments of the same product is a stronger portfolio artifact: the same problem, solved twice, on two clouds, with the reasoning written down for each.
Split across Vercel + Cloud Run An all-in-one platform is simpler to run but hides whether real containerization skills exist An actual Dockerfile and a genuine service boundary between two independently-deployed pieces demonstrates real multi-cloud competence, accepting the trade-off of two platforms to monitor and occasional Cloud Run cold starts.
Backend kept a monolith Mirroring a reference microservices codebase for a single-user chatbot with one request type is over-engineering Knowing when not to add a gateway/model/worker split is itself worth demonstrating — over-architecting a simple problem is a real anti-pattern interviewers watch for.
Gemini over Bedrock, Groq, OpenAI Bedrock is AWS-only (breaks the GCP story); Groq and OpenAI's free tiers are trial-credit only, not a standing free tier Gemini keeps the cloud story coherent end-to-end on GCP and has a genuine, ongoing free tier — a multi-provider fallback layer was considered and rejected as unnecessary vendor surface at this scope.
Firestore over S3-blob memory The AWS version used S3 mainly because it was already there, not because it fits structured, frequently-read-and-updated session records Porting the pattern — swap the memory backend behind an interface — mattered more than porting the exact implementation; a document database is simply the better fit.
Local Terraform state, single environment Mirroring the AWS repo's remote-state-plus-three-workspaces setup solves team and multi-environment problems This is a single-maintainer project — that infrastructure has no problem here to solve.
Workload Identity Federation on both clouds A downloaded service-account key is the default path and easy to reach for Keeping a consistent "no long-lived credentials" story across both cloud versions was a deliberate, matched design goal — not a coincidence.
Real Bugs

Cloud-provider gotchas hit while standing up twin-gcp

1
Leftover static export config broke the Vercel deploy. output: 'export' was needed on AWS because S3+CloudFront can only serve static files — but Vercel runs Next.js natively. Removing it was the correct architectural fix, not a workaround.
2
Site still 404'd after that fix. The Vercel project had been created without letting it auto-detect the framework, so it was configured as "Other" instead of "Next.js" — fixed permanently with a committed vercel.json rather than a one-off dashboard click.
3
GCP project ID collision and a transient 429 on project creation, resolved with a different ID and a polling retry.
4
Cloud Run "internal error" on a Terraform replace. Traced to deletion_protection = true blocking Terraform from replacing a failed revision — Terraform checks the live resource's protection flag at destroy time, not just the desired config, so the flag had to be disabled and the broken instance deleted directly.
5
Gemini billing-tier trap. Any GCP project with a billing account linked gets pushed into Gemini's paid tier, even for otherwise-free usage — GCP's free tier is usage-free, not billing-free. Fixed by issuing the Gemini key from a separate, fresh project with no billing account attached at all, since Google attributes usage to whichever project issued the key.
6
Model deprecation plus an IaC drift lesson. A pinned model ID was deprecated mid-project; after switching to a -latest alias, a quick out-of-band gcloud patch was silently reverted by the next terraform apply — fixed by making the model ID a properly Terraform-managed variable instead.
7
Cosmetic environment-label bug. The health check reported "environment": "development" even in production, because ENVIRONMENT was never actually set in Terraform — fixed by setting it explicitly.
Digital Twin — Solo Project, built twice on two clouds Next.js · FastAPI · Gemini · Bedrock · Cloud Run · Lambda · Terraform · GitHub Actions