The idea
The starting point was mine: I wanted to build something that could look at a photo, work out what was in it, and sort it into meaningful categories using AI. Working with my teammate Adriana, that idea got reshaped into something more practical and user-facing — a product review platform where every submitted image is automatically screened and labelled before it goes public. That pivot became ReviewReko, and it let me apply everything I’d originally wanted to build — image detection, label extraction, automated decision-making — inside a real product.
The problem it solves is a real one: marketplaces that let third-party sellers collect reviews have no automatic way to stop an inappropriate or irrelevant image from being published. Manual moderation doesn’t scale. ReviewReko screens every image the moment it’s uploaded and only publishes what passes.
Architecture
Once the concept was settled, I worked through the system architecture with my teammate Valerii, mapping the three layers — input, processing, and output — and which AWS service would own each responsibility.
Container-level architecture diagram, mapped out with Valerii before implementation.
- Input — an Amplify-hosted frontend, two API Gateway endpoints, and two Lambda functions that separate image upload from review metadata.
- Processing — event-driven: an S3 upload automatically triggers the ReviewProcessor Lambda, which calls Rekognition for moderation and label detection, then writes the result to DynamoDB and copies approved images to a public bucket.
- Output — a third Lambda and a GET endpoint serve approved reviews back to the gallery, with GSI-powered filtering by category.
What I built
The processing layer was my core responsibility — the part of the system where every submitted image is actually screened and classified before anything reaches the public gallery. Specifically, I built and configured:
- Both S3 buckets — the private uploads bucket and the public approved bucket, including bucket policies, CORS, and the S3 event notification that triggers processing on every new upload.
- The Rekognition integration — DetectModerationLabels to reject inappropriate content above a confidence threshold, and DetectLabels to extract content labels from approved images.
- The ReviewProcessor Lambda — the function that orchestrates the whole pipeline: reads the uploaded image, runs moderation, runs label detection, copies approved images to the public bucket, generates a plain-English description from the detected labels, and updates the DynamoDB record with the final status.
- DynamoDB status handling — making sure the record SaveReview writes on submission gets correctly updated by ReviewProcessor using the shared reviewId, so status moves cleanly from PENDING to APPROVED or REJECTED with full metadata attached.
- CloudWatch custom metrics — pushing a ProcessedImages metric with Status and Category dimensions after every moderation decision, feeding a dashboard of approval vs. rejection rates by category.
I also wrote most of the group report — architecture overview, business case, security justification, monitoring, and the design decisions below — and used the AWS Pricing Calculator to break down the projected annual cost per service.
In action
Submitting a review — a customer fills in product details, a star rating, and attaches a photo.
Browsing approved reviews — Rekognition’s detected labels power both the AI-generated description and the category tags, automatically.
Design decisions worth knowing
A few choices came up in review that are worth explaining, since they show the reasoning rather than just the result:
- Two S3 buckets, not one. Separating unmoderated uploads from approved images isn’t a preference, it’s a requirement — without it, either every image would need to be public (including ones later rejected), or approved images would need to be served through a Lambda, adding latency and cost for no benefit.
- No sort key on the DynamoDB table. All time-based queries are routed through the category-index GSI, which already has its own sort key — adding one to the base table would add write complexity with nothing to show for it.
- A GSI instead of a table scan for category filtering. A scan reads every record on every request regardless of match count; on DynamoDB’s on-demand pricing, that’s paying for reads you don’t need. The GSI reads only the matching category.
- update_item, not put_item, in ReviewProcessor. The record already exists by the time this Lambda runs (created by SaveReview on submission) — a put_item would silently overwrite the customer’s name, product details and rating. update_item with explicit attribute names touches only the fields this Lambda owns.
- The S3 event trigger is scoped to an uploads/ prefix. Without it, ReviewProcessor copying an approved image to the public bucket would itself fire a new event, triggering the function again — an infinite loop.
- Bedrock was descoped, not skipped by accident. The original plan used Bedrock (Claude Haiku) to generate the AI description. It’s blocked on student AWS accounts by an org-level service control policy, so the description is instead generated in Python directly from the Rekognition labels — a smaller but reliable substitute.