Overview
This project adds a persistent, floating AI chatbot to vargaskeo.com — a vanilla JS widget that pops open on any page and answers questions about my background, active projects, and resume. It steers visitors toward the contact form and maintains conversational context across a session. The full backend runs on AWS Free Tier services, making it essentially free at personal-site traffic levels.
Full Architecture
Two parallel flows: the content pipeline (GitHub → S3 → CloudFront) and the chat pipeline (Browser → API Gateway → Lambda → DynamoDB + S3 RAG + Claude Haiku).
How It Works
1 · Content Delivery: GitHub → S3 → CloudFront
Every git push triggers a GitHub Actions workflow that syncs changed files to S3 (####) and issues a CloudFront /* invalidation on distribution ####. Visitors always get the latest version from the nearest edge node — no stale cache, no .html extensions. The chatbot's context.json knowledge file follows the same deploy path.
2 · The Floating Widget
A single JS file injected by site.js renders a fixed chat icon on every page. It reads the active language from localStorage using the same key that i18n.js writes, so Claude responds in the visitor's current language without any extra configuration. A UUID is generated on first visit as the DynamoDB session key.
3 · API Gateway + Lambda
The widget POSTs to API Gateway, which enforces CORS and a 10 req/min usage plan. Lambda sanitizes the payload, reads context.json from S3 (the RAG source), fetches the session history from DynamoDB, assembles the full prompt with the cached system block, and calls Claude Haiku with the anthropic-beta: prompt-caching header.
4 · RAG via context.json
No vector database needed. A simple JSON file in S3 holds structured facts: bio, skills, project summaries, resume highlights, and the contact CTA. Lambda injects this as the static cached portion of the system prompt — written once, billed at 10% on every subsequent call thanks to Claude's prompt caching.
5 · Session Memory via DynamoDB
DynamoDB stores the last N message pairs keyed by session UUID, with a 1-hour TTL. Visitors get natural multi-turn conversations without anything stored permanently. The 25 GB free tier handles any personal-site volume comfortably.
Cost Breakdown
At personal-site traffic, this stack costs effectively nothing. The only real variable cost is Claude API tokens, minimized by Haiku pricing + prompt caching + output caps.
| Service | Free Tier allowance | Est. / month |
|---|---|---|
| Lambda | 1M requests + 400K GB-s/mo | ~$0 |
| API Gateway | 1M calls/mo (first 12 months) | ~$0 |
| DynamoDB | 25 GB + 200M requests/mo | ~$0 |
| S3 | 5 GB + 20K GET/mo | ~$0 |
| CloudFront | 1 TB egress + 10M requests/mo | ~$0 |
| Claude Haiku 4.5 | $1/$5 per MTok · cached input 90% off · 300-token output cap | < $0.50 |
Security Guardrails
Multiple independent layers harden the system against prompt injection, jailbreaks, and abuse:
- Input sanitizer — strips HTML,
<script>blocks, SQL patterns, and known injection phrases before anything reaches the model. - Locked system prompt — built entirely server-side in Lambda. Users cannot read, override, or append to it.
- Token caps — 500 tokens in, 300 out. Limits cost exposure and long-context adversarial attack surface.
- Output filter — Lambda scans Claude's response for unexpected code blocks, external URLs, or PII before returning it to the browser.
- API Gateway rate limiting — usage plan enforces 10 requests per minute per API key.
- No secrets in the frontend — Claude API key, S3 bucket name, and DynamoDB table name never appear in the browser. All credentials live in Lambda environment variables.
Build Order
- Step 1 — Author
context.json: bio, skills, projects, resume bullets, contact CTA in EN and FR. Upload to S3. - Step 2 — Write
chat_handler.pyLambda: sanitizer, S3 read, DynamoDB session, prompt builder with caching header, Claude call, output filter. - Step 3 — Create a DynamoDB table with
session_idas the partition key and a TTL attribute. - Step 4 — Wire API Gateway: new REST API, POST /chat resource, Lambda proxy integration, CORS, usage plan (10 rpm), stage deploy.
- Step 5 — Build widget JS: floating icon, chat panel, session UUID in
localStorage, language detection via shared storage key, fetch to API Gateway endpoint. - Step 6 — Inject widget via
site.js. Push to GitHub. Actions syncs to S3 and invalidates CloudFront. Widget is live on all pages.
// Built by Boris Vargas · vargaskeo.com · 2026