Skip to content
keegan@portfolio:~$

How this site is built

This portfolio is itself a project: a small but production-shaped serverless system on AWS, defined entirely as code.

Visitor
Browser
Route 53 + ACM
DNS & TLS
CloudFront
CDN · security headers · URL rewrite function

/*

S3 (private)
Static Next.js export via OAC

/api/*

API Gateway
HTTP API · throttling
Lambda · Node.js 22
chat · contact · visits · health
Amazon Bedrock
Nova 2 Lite · tool use
DynamoDB
Single table · TTL · streams
SES
Contact notifications
Everything is serverless, defined in AWS SAM and deployed by GitHub Actions using OIDC.

Design decisions

Static frontend, serverless backend

The Next.js site is statically exported and served from a private S3 bucket through CloudFront with Origin Access Control. There are no servers to patch, and hosting costs cents.

Same-origin API through CloudFront

CloudFront routes /api/* to an API Gateway HTTP API. The browser never makes a cross-origin call, so production needs no CORS. A secret origin header ensures the API only accepts traffic that came through CloudFront.

A bounded, grounded AI agent

The chat Lambda runs a tool-use loop on the Bedrock Converse API. Tools are typed with zod, and the JSON schemas the model sees are generated from the same definitions. The loop is capped at 5 iterations, and answers must be grounded in tool results.

Cost controls by design

Bedrock spend is bounded three ways: API Gateway throttling, a per-IP hourly limit and a global daily quota, each enforced with atomic DynamoDB counters that expire via TTL. An AWS Budget emails me before costs drift.

Event-driven notifications

Contact submissions are written to DynamoDB. A stream with event filtering invokes a notifier Lambda only for new contact items. It uses partial batch responses and an SQS dead-letter queue, so a failed email never blocks or duplicates the others.

Keyless CI/CD

GitHub Actions assumes a narrowly scoped IAM role via OIDC. The role can only drive CloudFormation through a separate execution role, sync site files and invalidate the CDN. No long-lived AWS keys exist anywhere.