cloudwithshad
๐Ÿ†
CAPSTONE PROJECT ยท BUILD-ON-YOUR-OWN

AI Customer Feedback Hub

The final project. A real production system you'll show employers. Customers submit feedback through a web form โ†’ AI analyzes it โ†’ you see the dashboard. This combines everything from weeks 1-4 into one portfolio piece.

5
PHASES
3-5 hours
DURATION
FREE
TIER OK
6 services
CHAINED
๐Ÿš€

This is YOUR capstone

The capstone has one rule: it must work end-to-end. How you get there is up to you. We provide the architecture, the success criteria, and the starter code. The implementation details are your portfolio piece. Stuck? Use the hints (๐Ÿ’ก). Beat the lab? Submit a deployed URL.

1
What you're building

A real production system with three parts:

  1. A public web form where anyone can submit feedback (name, email, message)
  2. An AI backend that analyzes every submission for sentiment and named entities
  3. A dashboard showing aggregate stats: sentiment breakdown, top entities, recent submissions
Customer submits form S3 (Frontend) static web form API call API Gateway POST /feedback Lambda ฮป process-feedback Comprehend sentiment, entities DynamoDB feedback table S3 (Dashboard) stats + charts You (Admin)
Two front-ends (form + dashboard). One backend pipeline. Everything is serverless.

Why this is a great portfolio piece:

  • It's a complete app โ€” frontend, backend, database, AI, all wired together
  • It uses 6+ AWS services in a real-world pattern
  • Solo developers can build it in a weekend
  • It demonstrates the most-demanded skill in cloud: service integration
2
Phase 1 โ€” The submission form (Frontend)

Build a simple HTML form. Single file, vanilla JS, no frameworks needed. Host it on S3 like Week 1.

1
Create a new S3 bucket named cloudwithshad-capstone-form-<yourname>
2
Enable static website hosting (Properties โ†’ Static website hosting โ†’ Enable, index = index.html)
3
Unblock public access + add public-read bucket policy
4
Upload a single index.html file (starter code below)
5
Open the bucket website URL โ€” you should see your form

Here's a clean starter HTML form (style it however you want โ€” make it look like YOU):

<!DOCTYPE html> <html> <head> <title>Customer Feedback</title> <style> body { font-family: sans-serif; max-width: 500px; margin: 60px auto; padding: 24px; background: #0A1628; color: #fff; } h1 { color: #00B4D8; } label { display: block; margin: 16px 0 6px; font-weight: 600; } input, textarea { width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #374151; background: #1A2F4A; color: #fff; font-size: 14px; } textarea { min-height: 100px; } button { background: #00B4D8; color: #0A1628; padding: 12px 24px; border: none; border-radius: 6px; font-weight: 700; cursor: pointer; margin-top: 16px; } </style> </head> <body> <h1>โ˜๏ธ Tell us how we did</h1> <form id="feedback-form"> <label>Your name</label> <input type="text" id="name" required> <label>Your email</label> <input type="email" id="email" required> <label>Your feedback</label> <textarea id="message" required></textarea> <button type="submit">Submit feedback</button> <div id="status" style="margin-top:14px;"></div> </form> <script> document.getElementById('feedback-form').addEventListener('submit', async (e) => { e.preventDefault(); const data = { name: name.value, email: email.value, message: message.value }; // TODO: replace with your API Gateway URL after Phase 4 const API_URL = 'YOUR_API_GATEWAY_URL_HERE'; const res = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); status.textContent = res.ok ? 'โœ“ Thank you!' : 'Something went wrong.'; if (res.ok) e.target.reset(); }); </script> </body> </html>
Make it yours

The starter is intentionally plain. Customize the design, the colors, the copy โ€” make it feel like your brand. This is a portfolio piece. Employers notice taste.

3
Phase 2 โ€” DynamoDB table
1
DynamoDB โ†’ Create table
2
Table name: cloudwithshad-feedback
3
Partition key: feedback_id (String)
4
Defaults: on-demand, no sort key
5
Create table

Items will store: id, name, email, message, sentiment, entities, timestamp.

4
Phase 3 โ€” The Lambda + IAM role

First, the IAM role. Lambda needs to call Comprehend and write to DynamoDB.

  1. IAM โ†’ Create role โ†’ AWS service โ†’ Lambda
  2. Attach: AWSLambdaBasicExecutionRole, ComprehendFullAccess, AmazonDynamoDBFullAccess
  3. Role name: cloudwithshad-feedback-role

Now the Lambda function:

  1. Lambda โ†’ Create function โ†’ Author from scratch
  2. Name: cloudwithshad-process-feedback
  3. Runtime: Python 3.12 | Role: cloudwithshad-feedback-role
  4. Paste the code below, then Deploy
import boto3 import json import uuid from datetime import datetime comprehend = boto3.client('comprehend') dynamo = boto3.resource('dynamodb') table = dynamo.Table('cloudwithshad-feedback') # CORS headers so the form can call this Lambda from S3 CORS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'POST, OPTIONS', } def lambda_handler(event, context): # Handle preflight OPTIONS request if event.get('httpMethod') == 'OPTIONS': return {'statusCode': 200, 'headers': CORS, 'body': ''} try: body = json.loads(event['body']) name = body['name'] email = body['email'] message = body['message'][:5000] # Comprehend limit # Sentiment analysis sentiment = comprehend.detect_sentiment(Text=message, LanguageCode='en') # Entity extraction entities_raw = comprehend.detect_entities(Text=message, LanguageCode='en') entities = [ {'text': e['Text'], 'type': e['Type']} for e in entities_raw['Entities'][:10] ] # Save to DynamoDB feedback_id = str(uuid.uuid4()) table.put_item(Item={ 'feedback_id': feedback_id, 'name': name, 'email': email, 'message': message, 'sentiment': sentiment['Sentiment'], 'sentiment_score': str(sentiment['SentimentScore'][sentiment['Sentiment'].title()]), 'entities': entities, 'submitted_at': datetime.utcnow().isoformat() }) return { 'statusCode': 200, 'headers': CORS, 'body': json.dumps({'success': True, 'id': feedback_id, 'sentiment': sentiment['Sentiment']}) } except Exception as e: return { 'statusCode': 500, 'headers': CORS, 'body': json.dumps({'error': str(e)}) }
Bump timeout

Configuration โ†’ General โ†’ Edit โ†’ Set Timeout to 30 seconds. Comprehend calls can take several seconds.

5
Phase 4 โ€” Wire it up with API Gateway

The browser can't call Lambda directly โ€” we need a public HTTP endpoint. API Gateway gives us exactly that.

1
API Gateway โ†’ Create API โ†’ HTTP API โ†’ Build
2
API name: cloudwithshad-feedback-api
3
Integrations: Add integration โ†’ Lambda โ†’ pick cloudwithshad-process-feedback
4
Configure routes: Method = POST, Resource path = /feedback
5
Stage: $default (auto-deploy)
6
Create. Copy the Invoke URL at the top of your API page
7
Add CORS: API โ†’ CORS โ†’ Allow origin: *, Allow methods: POST, OPTIONS, Allow headers: Content-Type
8
Paste the full URL (Invoke URL + /feedback) into your index.html at the API_URL placeholder
9
Re-upload index.html to your S3 form bucket
Hint: testing if your API endpoint works

From your laptop terminal:

curl -X POST https://YOUR-API-URL/feedback -H "Content-Type: application/json" -d '{"name":"Test","email":"a@b.c","message":"Great service!"}'

If you get back {"success": true, "sentiment": "POSITIVE", ...} โ€” the API works. Check your DynamoDB table for the new item.

Test it live

Open your S3 form URL in a browser. Submit some feedback. Check DynamoDB. You just built a real production AI app. Take a screenshot of the form + the DynamoDB item that appeared.

6
Phase 5 โ€” Build the admin dashboard

The final piece. A separate webpage (also hosted on S3) that fetches the data and shows aggregate stats.

Two ways to do this:

  1. Easy: Add a GET route to your existing API that scans DynamoDB and returns all feedback. The dashboard calls it.
  2. Harder (better!): Use Amazon Athena to run SQL queries over DynamoDB-exported S3 data. More work, more impressive.

For the bootcamp, go with option 1. Add a second Lambda function:

import boto3 import json from collections import Counter from decimal import Decimal dynamo = boto3.resource('dynamodb') table = dynamo.Table('cloudwithshad-feedback') class DecimalEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Decimal): return float(obj) return super().default(obj) def lambda_handler(event, context): # Scan all feedback (fine for small datasets) response = table.scan() items = response['Items'] # Aggregate sentiment counts sentiment_counts = Counter(item['sentiment'] for item in items) # Top entities entity_counts = Counter() for item in items: for e in item.get('entities', []): entity_counts[e['text']] += 1 top_entities = entity_counts.most_common(10) return { 'statusCode': 200, 'headers': {'Access-Control-Allow-Origin': '*'}, 'body': json.dumps({ 'total': len(items), 'sentiment_breakdown': dict(sentiment_counts), 'top_entities': top_entities, 'recent': sorted(items, key=lambda x: x.get('submitted_at', ''), reverse=True)[:10] }, cls=DecimalEncoder) }

Add a new route in API Gateway (GET /stats โ†’ this Lambda), then create a dashboard.html that fetches and renders the data. Get creative with charts โ€” use Chart.js (one CDN link, beautiful results).

Hint: simple Chart.js dashboard structure

Add <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> to your HTML. Then call your /stats endpoint with fetch(), and render a doughnut chart for sentiment plus a horizontal bar chart for top entities. The Chart.js docs have copy-paste examples for every chart type.

๐Ÿ“ฆ Portfolio submission requirements

This is your capstone deliverable. Submit a single message in the cohort WhatsApp group with:

  • The S3 URL of your live form (someone in the group will test it!)
  • The S3 URL of your dashboard
  • A GitHub repo link with all your HTML, Lambda code, and a README explaining the architecture
  • One screenshot of the dashboard showing real data after a few submissions

Bonus points for: custom design, a README that's well-written, a deployed CloudFront distribution in front of S3, and a brief architecture diagram.

๐Ÿš€ Make it employer-impressive

Want to really stand out? Try one or more:

  • SNS notifications: Send yourself an email every time NEGATIVE feedback comes in
  • Multi-language: Detect language with Comprehend first, then call Translate to convert to English before analysis
  • Auth: Protect the dashboard behind Cognito so only you can see the data
  • CI/CD pipeline: Hook up GitHub Actions (like Week 3 Lab 6) so pushes auto-deploy
  • CloudFront + custom domain: Hook up a real domain name (Route 53 + ACM cert)
  • Custom Comprehend classifier: Train a model to categorize feedback by topic (billing, support, product, etc.)
7
Clean up (or keep it running!)

This setup is genuinely cheap to keep running. Lambda + API Gateway + S3 + DynamoDB at zero traffic costs basically nothing. Many students choose to leave it up as their portfolio.

If you want to keep it running:

  • Set a billing alarm at $1 so you'll know if anything spikes
  • Rotate the IAM keys every 90 days
  • Add basic auth to the dashboard so it isn't world-readable

If you want to delete everything:

  • API Gateway โ†’ APIs โ†’ delete the API
  • Lambda โ†’ delete both functions
  • DynamoDB โ†’ delete the table
  • S3 โ†’ empty + delete both buckets (form + dashboard)
  • IAM โ†’ delete the role
๐Ÿ†

You did it.

You started 4 weeks ago โ€” possibly never having touched AWS. Today you finished a real production AI application. That is genuinely rare.

Take a screenshot of your live form. Save your GitHub link. Add it to your LinkedIn under "Projects". Apply for cloud roles with confidence โ€” because you've already built what they're asking about.

The cloud is yours now. Go ship something.