An AI twin recruiters can talk to directly about my background, skills, and projects, grounded in my actual experience rather than generic chat. Built and deployed twice, on two different clouds, as a deliberate portfolio strategy.
Google GeminiInfrastructure as CodeMulti-CloudCI/CD
The AWS/Bedrock deployment is intentionally kept offline to avoid ongoing cloud costs. The live demo above runs on the GCP + Vercel deployment instead.
Technical Deep Dive
Core serviceThird-party APIData storeClient / UI
For the full detail beyond what fits here, these are standalone interactive diagrams (they open in a new tab, sized for a wide screen):
A Next.js frontend on Vercel calls a FastAPI backend on Google Cloud Run, which calls Gemini (`gemini-flash-latest`) for generation and reads/writes conversation history in Firestore. The Gemini API key is held in Secret Manager and read by Cloud Run at runtime rather than baked into the container or passed as a plain env var. GitHub Actions builds and deploys the backend on every push to `main` that touches backend code, authenticating to GCP via Workload Identity Federation rather than a downloaded service-account key: no long-lived cloud credentials stored anywhere.
The backend is a deliberate monolith: one FastAPI service with typed, env-driven config (`pydantic-settings`), a typed exception hierarchy so every error returns the same JSON shape, route logic kept separate from app wiring, and a real test suite with the Gemini client mocked out.
The AWS sibling version (not currently live) took a different shape entirely: a statically-exported Next.js frontend behind CloudFront + S3, a FastAPI backend adapted to Lambda via `mangum` behind API Gateway, Bedrock for generation, and S3 for conversation memory; provisioned across three separate Terraform workspaces (dev/test/prod) with remote state in S3 and a DynamoDB lock table.
Why a Second, Separate Deployment
This is deliberately a brand-new sibling repo (`twin-gcp`) rather than a migration of the original AWS version (`twin`): the goal was a portfolio artifact, not a cloud switch. As the repo's own architecture notes put it: "the goal is a portfolio, not a migration: two live, working, differently-architected deployments of the same product is a stronger portfolio artifact than one deployment that replaced the other." The AWS version isn't currently running (the repo's own cost-teardown guidance recommends destroying unused environments and leaving only near-zero-cost resources in place), while this GCP + Vercel version is the live one.
Design Decisions
Split across two platforms (Vercel + Cloud Run) rather than one all-in-one option, specifically to show real containerization (an actual Dockerfile, not a framework auto-wrapping the code) and a genuine service boundary between two independently-deployed pieces: accepting the real trade-off of two platforms to monitor and occasional Cloud Run cold starts, since the goal here is demonstrating breadth, not the simplest way to ship a chatbot
Kept the backend a monolith rather than mirroring a reference microservices codebase: a single-user chatbot with one request type doesn't need a gateway/model/worker split, and knowing when *not* to add that complexity was treated as worth demonstrating in its own right, since over-architecting a simple problem is a real anti-pattern
Chose Gemini over Bedrock (AWS-only), Groq, or OpenAI (trial-credit only) to keep the cloud story coherent (GCP end-to-end) and because Gemini has a genuine standing free tier; a multi-provider fallback layer was considered and rejected as unnecessary extra vendor surface for this project's scope
Chose Firestore over replicating the AWS version's S3-blob approach for conversation memory: the AWS version used S3 mainly because it was already there, not because it's a good fit for structured, frequently-read-and-updated records; porting the *pattern* (swap the memory backend behind an interface) mattered more than porting the exact implementation
Used local Terraform state and a single environment rather than mirroring the AWS repo's remote-state-plus-three-workspaces setup: that infrastructure exists to solve team and multi-environment problems a single-maintainer project doesn't have
Workload Identity Federation over a downloaded service-account key on both cloud versions: keeping a consistent "no long-lived credentials" story across both was itself a deliberate goal, not a coincidence
Problems & Challenges
A leftover `output: 'export'` from the AWS version's Next.js config (needed there because S3+CloudFront can only serve static files) actively broke the Vercel deploy, since Vercel runs Next.js natively: removing it was the correct architectural fix, not a workaround
Even after that fix, the site still 404'd: 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
Provisioning GCP for real hit a chosen project ID that was already globally taken, a transient `429` on project creation, and a Cloud Run "internal error" that turned out to be `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 before Terraform would proceed
Wiring the real Gemini key surfaced a genuine billing-tier trap: any GCP project with a billing account linked gets pushed into Gemini's paid tier, even for otherwise-free usage, because GCP's free tier is usage-free, not billing-free: the fix was 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, not whichever server calls it
A pinned model ID (`gemini-2.5-flash`) was deprecated for new API keys mid-project; after switching to the `-latest` alias, a quick out-of-band `gcloud` patch to set the model env var was silently reverted by the next `terraform apply`: a general IaC lesson (anything changed outside the IaC tool eventually gets erased by it) fixed by making the model ID a properly Terraform-managed variable
A cosmetic but telling bug: the health check reported `"environment": "development"` even in the production deployment, because `ENVIRONMENT` was never actually set in Terraform; fixed by setting it explicitly rather than assuming a default that was never there
Limitations & Improvements
After hitting a transient `503` (Gemini at capacity) during testing, the question worth asking was whether depending on a free-tier LLM API was the wrong call: rather than switching providers, the friction was separated into one-time setup mistakes (now fixed) versus a `503` that's a normal, ongoing characteristic of calling any hosted LLM API, which switching providers wouldn't eliminate. The fix was retry-with-backoff around the Gemini call, specifically for the transient codes (`429`, `500`, `503`), while non-transient errors (bad key, unknown model) still fail immediately rather than wasting time retrying something retrying can't fix. Three tests cover exactly these paths.
Single environment and local Terraform state are explicit scope decisions for a single-maintainer demo, not gaps: a team project would warrant remote state and dev/test/prod separation the way the AWS sibling has
Cloud Run's scale-to-zero means occasional cold-start latency on infrequent traffic: an acceptable trade for a portfolio demo, not one that would hold up for a real product's SLA
GitHub Actions secrets/variables for the GCP deploy are repo-scoped rather than environment-scoped, since the workflow doesn't declare a GitHub `environment:` property; fine at this project's single-environment scale, but worth revisiting if that ever changes