Skip to content

quantumleeps/secure-portfolio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Secure Portfolio

A hosted portfolio website with private, tracked access. Each job application includes a unique tracking link. When a recruiter or hiring manager visits, the system validates the link server-side, serves a role-specific slide-based presentation, and records engagement metrics. Invalid or missing links return a 404. No API keys, validation logic, or tracking details are ever exposed to the client.

How It Works

  1. A unique link is generated for each job application (e.g., https://danleeper.com/portfolio?r=spotify-382z3k).
  2. The recruiter clicks the link. The Next.js app makes a server-side call to API Gateway.
  3. A Lambda function queries DynamoDB to validate the ?r= slug.
  4. If the slug is valid, Lambda returns the portfolio slides for that link's assigned role version, generates time-limited pre-signed URLs for each slide's images, and records the visit.
  5. If the slug is invalid or missing, the app renders a 404 page.
  6. Once the page loads, a lightweight client-side heartbeat pings the backend every 30 seconds to track time-on-page and engagement.

Architecture

The system has two runtime flows, both routed through the same API Gateway.

Page load (server-side rendering) — When a recruiter clicks a tracking link, the request hits AWS Amplify, which serves the Next.js app via CloudFront and Lambda@Edge. During SSR, the Next.js server component makes a GET /api/portfolio?r={slug} call to API Gateway, which invokes the validate-link Lambda. That function checks the slug against the tracking-links DynamoDB table, looks up the role version's slide order, batch-fetches the corresponding slides, generates pre-signed S3 URLs for each slide's images (10-minute expiry), records the visit, and returns the ordered slide data with signed image URLs. Next.js renders the portfolio HTML and sends it to the browser. Nothing about the validation logic, tracking, or API is exposed to the client.

Heartbeat (client-side) — Once the page loads in the browser, a lightweight heartbeat fires a POST /api/heartbeat to API Gateway every 30 seconds while the tab is active. The record-heartbeat Lambda increments the heartbeat counter on the visit record in DynamoDB. This is the only client-to-backend call that occurs after the initial page load.

Data layer — Three DynamoDB tables store all application state: portfolio-slides holds slide content (title, narrative sections, tech tags, image keys), role-versions maps each role type to an ordered list of slide IDs, and tracking-links stores slugs with their visit history and engagement data. Portfolio images are stored in a private S3 bucket with all public access blocked — they are never directly accessible and can only be viewed through pre-signed URLs generated during a valid tracking link visit.

Hosting — AWS Amplify manages the CloudFront distribution, Lambda@Edge for SSR, and S3 for static assets. Two environments: dev (development branch, *.amplifyapp.com) and prod (main branch, custom domain via Route 53). A CloudFront WAF with rate limiting and AWS managed rule groups protects the prod frontend.

IAM — Prod deployments use GitHub Actions OIDC — no static credentials. Two OIDC roles: github-deployer (Terraform for prod) and prod-github-operator (data operations). Dev uses scoped local IAM users (dev-deployer, dev-operator) with access keys stored in SSM Parameter Store.

Security hardening — S3 versioning with noncurrent version lifecycle (30d to IA, 90d expire) and TLS-only bucket policy. DynamoDB point-in-time recovery on all tables. API Gateway native throttling (burst/rate limits). WAF rate limiting + AWS managed rule groups on CloudFront.

Tech Stack

  • Frontend: Next.js (SSR, App Router)
  • Hosting: AWS Amplify (CloudFront, Lambda@Edge, S3 — managed)
  • Backend: AWS Lambda, API Gateway, DynamoDB, S3
  • IaC: Terraform
  • DNS: Route 53
  • Security: WAF v2 (CloudFront), OIDC (GitHub Actions)
  • CI/CD: GitHub Actions (OIDC for prod, manual for dev)
  • CLI Tools: Node scripts

Project Structure

secure-portfolio/ ├── app/ # Next.js portfolio application ├── iac/ # Terraform infrastructure-as-code │ ├── bootstrap/ # IAM, OIDC provider, state backend, SSM keys │ ├── environments/ │ │ ├── dev/ # Dev environment (development branch) │ │ └── prod/ # Prod environment (main branch, custom domain) │ └── modules/ │ ├── amplify/ # Amplify app + optional domain association │ ├── api-gateway/ # HTTP API v2 with throttling │ ├── dns/ # Route 53 hosted zone │ ├── dynamodb/ # DynamoDB table with PITR │ ├── lambda/ # Lambda function │ ├── s3-private/ # S3 bucket with versioning, lifecycle, TLS-only │ └── waf/ # CloudFront WAF with rate limiting ├── scripts/ # CLI tools for link management and metrics ├── .github/workflows/ # CI/CD (deploy-infra, deploy-frontend, operate) ├── .gitignore └── README.md 

Key Concepts

Slide-Based Presentation — The portfolio is a series of slides, each showcasing a project or topic. Each slide contains a title, subtitle, narrative sections (challenge, what was built, impact), tech stack tags, and images. Slide content is stored as JSON in DynamoDB. Images are stored in a private S3 bucket and served via pre-signed URLs that expire after 10 minutes — the frontend prefetches all images on page load so expiry only needs to cover the initial fetch, not the full viewing session.

Role-Based Versions — The portfolio exists in 2-3 variants tailored to different role types (e.g., technical_architect, full_stacK_engineer). Each version defines an ordered list of slide IDs — controlling which slides appear and in what sequence. All versions draw from the same pool of slides.

Tracking Links — Every link contains a unique slug tied to a specific company and role version. Links are managed via a CLI script and stored in DynamoDB. A link that has not been created returns a 404 — there is no generic or fallback portfolio.

Engagement Metrics — After the portfolio renders, a lightweight heartbeat fires every 30 seconds to record time-on-page. Visit metadata (timestamp, referrer, user-agent) is captured on the initial server-side validation call. All tracking data is queryable via a CLI script.

Scripts

CLI tools in scripts/ for managing links and reviewing engagement. See scripts/README.md for full usage.

  • manage-links — Create, list, and revoke tracking links. Writes to DynamoDB directly using the operator AWS profile.
  • view-metrics — Query visit and engagement data from DynamoDB. Supports filtering by slug or company.
  • seed — Populate DynamoDB tables with slide content, role versions, and initial tracking links.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors