production — serving this page right now

A serverless static site, running on managed AWS services only.

No servers to patch or scale. S3 holds the files, CloudFront distributes and secures them, and a Lambda function backed by SES handles the contact form on the main portfolio site.

// how you're reading this page, right now
You
Visitor
HTTPS request
CDN
CloudFront
edge · cache
S3
Private S3
via OAC

Stack

Every piece of this project is a managed AWS service, wired together with a scoped IAM role and documented reasoning for each choice.

S3 CloudFront + OAC Route 53 ACM Lambda SES IAM (least privilege) AWS CLI

How it's built, and why

Recorded as they were made, in the format used to document architecture decisions in practice — including the one that got revisited.

ADR-001 Private S3 bucket + CloudFront with Origin Access Control +

Decision: the S3 bucket is never public. Public access is explicitly blocked on all four settings (ACLs, bucket policy, public bucket). The only way to read the files is through CloudFront, authorized via Origin Access Control (OAC).

Why: a public bucket exposes files to direct reads from anyone who knows the URL, with no caching, no automatic HTTPS, no protection from abuse (hotlinking, mass scraping that drives up S3 request costs). Routing everything through CloudFront centralizes all of this: free HTTPS via a managed certificate, distribution from global edge locations, and a single entry point to secure instead of two.

ADR-002 Dedicated IAM role, assumed via AssumeRole +

Decision: no operation on this project uses a broadly-permissioned IAM user. There's a dedicated role (Role-StaticSite) with a policy listing only the actions needed on explicitly named resources — no generic wildcards. The base account user assumes this role only when needed, getting temporary credentials.

Why: this is the least-privilege principle applied concretely, not just in theory: if a credential for this project were ever compromised, the blast radius is limited to that role's permissions — no access to other projects, no access to unused services here. It's also the pattern used in real multi-project company environments.

ADR-003 eu-central-1 region, except for CloudFront certificates +

Decision: all resources (S3 bucket, Lambda function) are created in Frankfurt (eu-central-1). CloudFront remains a global service, not tied to a region.

Why: lower latency serving European traffic. The one hard technical constraint: TLS certificates for CloudFront must be requested in us-east-1 — not negotiable, regardless of where the rest of the infrastructure lives.

ADR-004 No custom domain, initially + superseded by ADR-007

Decision: the site initially ran on CloudFront's default domain, with no Route 53 and no registered domain.

Why: at the time, a custom domain added no technical value for validating the S3 → CloudFront → Lambda/SES pipeline, only a recurring cost. This was reversed once SES's production-access requirements made a verified domain necessary — see ADR-007.

ADR-005 Contact form via Lambda + SES, not a third-party service +

Decision: email delivery is handled by a Lambda function calling Amazon SES directly, instead of an external form service.

Why: the point is demonstrating command of the AWS serverless stack directly. SES also comes with a real constraint worth handling — sandbox mode, which restricts sending to verified addresses only until production access is granted — which is itself part of the exercise. The form itself now lives on the main portfolio page; this project provides the backend.

ADR-006 Infrastructure built via AWS CLI, not Infrastructure as Code +

Decision: every resource in this project was created with direct AWS CLI commands, not CloudFormation or Terraform.

Why: building by hand first forces a real understanding of what each resource actually does — bucket policies, IAM trust policies, OAC configuration — instead of hiding it behind a template. This project is a strong candidate for a later Terraform rebuild as a consolidation exercise.

ADR-007 Revisiting ADR-004: a custom domain, after all +

Decision: registered luigisantoro.dev and wired it up through Route 53, ACM, and CloudFront — reversing ADR-004.

Why: the contact form's "send a technical breakdown" option only works with SES in production mode, and production access requires a verified domain identity — a single verified email address isn't enough. Registering a domain ($12-15/year) unlocked it properly, and doubled as a Route 53 + ACM exercise. This project — along with subsequent ones — now lives on a subdomain of that same registered domain.