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.
Every piece of this project is a managed AWS service, wired together with a scoped IAM role and documented reasoning for each choice.
Recorded as they were made, in the format used to document architecture decisions in practice — including the one that got revisited.
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.
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.
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.
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.
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.
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.
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.