Portfolio Chatbot

Type Personal Project
Year 2026
Platform Web (Embedded Widget)
Stack Node.js · Claude API · Railway

A Claude-powered AI assistant embedded across this portfolio. Visitors can ask it questions about my background, projects, skills, and how to get in touch - and receive streamed, real-time responses. The chatbot is the widget in the bottom-right corner of every page.

The goal was to build this as a production-grade feature. The backend is a standalone API service with rate limiting, CORS, and streaming. The API key never touches the browser. The architecture mirrors how you'd ship an AI feature in a real product team.

The system is split into two separate repos: a static frontend (GitHub Pages) and a dedicated API service (Railway). This separation means the Anthropic API key is never shipped to the browser, CORS is locked to the portfolio domain, and the backend can be scaled or swapped independently.

Browser (sebastianbosman.com — GitHub Pages) ├─ js/chat.js vanilla JS widget, no dependencies └─ POST /chat ──────────────────────────────────────────────┐ portfolio-chat-api (Railway) ├─ CORS ──── locked to sebastianbosman.com ◄──┘ ├─ Rate limit ─ 20 requests / 15 min / IP ├─ Input validation ─ 500 char max, array bounds check ├─ System prompt ─ Sebastian's full profile (hardcoded) └─ SSE stream ──────────────────────────────────────────────┐ Anthropic API (Claude Haiku) ◄──┘ └─ API key: Railway env var — never in browser source
  • Streaming responses via Server-Sent Events (SSE) - tokens render as they arrive
  • Conversation history passed on each request - the bot remembers context within a session
  • Rate limited to 20 requests per 15 minutes per IP - protects against abuse and API cost spikes
  • CORS locked to sebastianbosman.com - the endpoint can't be called from other origins
  • Input capped at 500 characters and validated server-side
  • Suggested questions on first open - reduces friction for new visitors
  • Animated thinking state, streaming cursor, and error handling with recovery
  • Fully responsive - works on mobile, adapts panel width to screen size
  • Zero frontend dependencies - pure vanilla JS, no framework or bundler
  • Haiku over Sonnet - Response speed matters more than depth for a portfolio chat widget. Haiku is faster and cheaper, which is the right trade-off at this scale.
  • SSE over WebSockets - SSE is simpler, HTTP-native, and sufficient for one-way streaming. WebSockets add connection overhead with no benefit here.
  • Hardcoded system prompt over RAG - My profile fits in a single prompt. RAG adds infra complexity (vector DB, embeddings pipeline) that isn't justified at this scope - but the architecture could support it.
  • Separate backend repo - Keeps secrets out of the frontend repo, enables independent deployment and scaling, and reflects how production teams actually ship this kind of feature.
  • No auth - Rate limiting per IP is sufficient for a public portfolio. Adding auth (e.g. JWT) would be the next step if this were a multi-tenant product.
Node.js Express Claude API (Haiku) SSE Streaming express-rate-limit CORS Railway Vanilla JS GitHub Pages Claude Code
  • Spec'd the architecture before writing code: separate API service, SSE streaming, no frontend dependencies
  • Built the Express backend with rate limiting and CORS first - got the security layer right before the AI layer
  • Designed the system prompt to be concise but complete - covering work history, skills, projects, tone, and what to do when the bot doesn't know something
  • Built the frontend widget in vanilla JS to match the existing portfolio design system - CSS variables, Inter font, dark theme tokens
  • Deployed backend to Railway with the API key as an environment variable - never committed to source
  • Wired the widget into all 16 portfolio pages via a single script tag