From a Terminal App to a Real Cloud Website — What Building with AWS Kiro, Flask, DynamoDB and SNS Taught Me
Building a CLI app is one thing. Giving it a real website, a cloud database, and automatic email notifications is another. Here is how we did all three using AWS Kiro
Something Changed When the Email Arrived
I was recording a tutorial. Screen capture running. Talking through the steps. We had just clicked Add Task on a simple web page we had built — a Flask app connected to AWS DynamoDB, all built using AWS Kiro.
Then I opened the email inbox on screen.
An email was there. From AWS. Subject line: New Task Added — TodoApp. Body: the exact title of the task we had just typed. Sent automatically. No extra code. No manual step. Just a single button click that triggered a chain of events — Flask received the request, DynamoDB saved the task, SNS published a notification, and the email landed in an inbox — all within a second.
"One button click. Flask caught it. DynamoDB saved it. SNS broadcast it. An email arrived. Four things. One second. Zero manual steps. That is cloud architecture in practice."
What We Had Already Built — The Journey to Part 4
Part 4 did not arrive in isolation. It was the fourth step in a series that built deliberately, one layer at a time. If you are reading this without context, here is the quick version.
Part 1 — We built a Todo CLI app in Python using AWS Kiro's spec-driven development workflow. Describe what you want in plain English, Kiro generates a full specification — requirements, design, tasks — you review and approve it, then Kiro writes the code. The app ran in the terminal and saved tasks to a local file.
Part 2 — We added steering files and hooks. Steering files give Kiro permanent memory of your project standards, technology stack, and coding conventions. Hooks automate quality checks — running tests automatically when files are saved, adding docstrings when functions are missing them. The development workflow became professional and consistent.
Part 3 — We replaced the local file with AWS DynamoDB. Tasks now saved to a real cloud database. Close the app, restart the computer, run it from a different machine — the tasks were always there. The app became genuinely cloud-connected for the first time.
But it was still a CLI app. You still typed commands in a terminal to use it. It had a cloud backend but no real interface. That is what Part 4 fixed.
"Part 4 was about closing the gap between what we had built and what a real application actually looks like — something with a browser, buttons, and instant feedback."
What We Set Out to Build in Part 4
The goal was specific and visible. Take the Todo app, add a proper web frontend, and add email notifications. By the end of the video, a user would open a browser, see all their tasks loaded from DynamoDB, add a new task with a button click, receive an email confirmation from AWS, and delete tasks from the list — all without touching the terminal.
The architecture looks like this:
Browser (index.html + JavaScript)
↕ HTTP fetch requests
Flask web server (app.py)
↕ boto3
┌────┴────┐
DynamoDB SNS
(saves tasks) (sends emails)
Three layers. Four technologies. Two AWS services. And the entire thing built using plain English descriptions to Kiro.
Before I explain what we built, let me explain why each technology choice was made.
Why Flask — The Simplest Backend for This Job
Flask is a Python web framework. It lets you create a web server in Python with minimal setup and minimal boilerplate. For a project like this, it was the right choice for three specific reasons.
First — our entire project was already in Python. DynamoDB operations were in Python. The CLI was in Python. Adding a Flask web server meant staying in one language and reusing everything we had already built.
Second — Flask is genuinely beginner friendly. You can have a working web server in five lines of Python. Understanding what each line does is straightforward. There is no magic hiding the fundamentals.
Third — Flask plays well with boto3. Since both run in Python, the Flask routes can directly call the same DynamoDB and SNS functions we had already written in db.py. No translation layer. No complexity. Just Python calling Python.
"We did not choose Flask because it is the most powerful option. We chose it because it is the most transparent option — and transparency matters enormously when you are teaching."
Why SNS — And What It Actually Is
Amazon SNS — Simple Notification Service — is a fully managed messaging service. The core concept is publish and subscribe. You create a Topic. Things that want to send messages publish to that topic. Things that want to receive messages subscribe to it. SNS handles the delivery.
We added SNS to this project for a specific reason that goes beyond making the tutorial interesting.
Think about what a real team task manager looks like. A manager wants to know when a new task is assigned. A team lead wants an alert when something urgent is added. In a real application, notifications are not a nice-to-have — they are part of the workflow. By adding SNS here, in a simple project, we are introducing a pattern that appears in production systems everywhere.
The pricing matters too, especially for beginners worried about AWS costs. The SNS free tier covers 1,000 email deliveries per month permanently — not just for the first twelve months. For a learning project or a small side project, you will almost certainly never exceed this limit.
One important detail for accuracy: the 1,000 free email deliveries is specifically for email endpoint notifications. Mobile push notifications have a separate free tier of 1 million deliveries. These are different counters.
Everything We Set Up Before Calling Kiro
I am always transparent about prerequisites in my tutorials. Here is every setup step we completed before writing a single line of code.
Installing Flask
One command in the terminal, inside the project folder:
pip install flask
boto3 was already installed from Part 3. AWS credentials were already configured from Part 3. This was the only new installation.
Creating the SNS Topic
In the AWS Console, we navigated to Simple Notification Service and created a Standard topic named TodoAppNotifications. Standard topics offer high throughput and at-least-once delivery, which is what we need for email notifications. FIFO topics are for use cases requiring strict message ordering, which is not relevant here.
After creating the topic, we copied the Topic ARN — a string that looks like this:
arn:aws:sns:us-east-1:123456789012:TodoAppNotifications
This ARN is the unique address of our topic. It goes into the Python code so boto3 knows exactly where to publish messages. We saved it immediately — you use it multiple times.
Subscribing an Email Address
Creating a topic alone does nothing. You need subscribers. We created a subscription with Protocol set to Email and the endpoint set to a real email address. AWS immediately sends a confirmation email to that address. You must click the confirmation link before AWS will deliver any notifications. Until you confirm, the subscription status shows Pending Confirmation.
After confirming, the status changes to Confirmed. From that point, every message published to the topic arrives in that inbox.
Adding SNS Permission to the IAM User
We already had an IAM user — todo-app-user — with AmazonDynamoDBFullAccess from Part 3. We added AmazonSNSFullAccess to the same user. This gives our application permission to publish messages to SNS topics.
This step is easy to forget and causes a clear error when missed. If boto3 tries to publish to SNS without this permission, AWS returns an AccessDeniedException. The error message is specific enough that it is easy to diagnose, but it is cleaner to set the permission correctly before running the code.
Updating Steering Files — Setting the Context Before Building
This step rewards everyone who built Part 2 with us. Steering files in Kiro give the AI permanent memory of your project. Before we described anything to Kiro, we updated the tech.md steering file to include two new sections:
## Web Framework
- Use Flask as the web framework
- All routes go in app.py
- Keep all DynamoDB and SNS operations in db.py
- HTML templates go in a folder called templates/
- Use plain HTML, CSS and vanilla JavaScript only
- Use fetch() for all API calls from the browser
- Flask runs on port 5000
- All API responses must be JSON format
## Notifications
- Use AWS SNS for email notifications
- SNS Topic ARN: [your topic ARN]
- Send an SNS notification when a new task is added
- Include the task title in the notification message
- Add SNS code inside the add_task function in db.py
- Handle SNS errors with try/except — never crash if SNS fails
These two additions meant that every piece of code Kiro subsequently generated would automatically use Flask correctly, put files in the right places, produce JSON responses, call SNS on every new task, and handle failures gracefully. We set these rules once. Kiro followed all of them throughout the build.
The instruction to handle SNS errors gracefully deserves emphasis. If SNS fails — a network issue, a temporary AWS disruption, a permissions problem — we do not want the entire add task operation to fail. The task should still save to DynamoDB. The SNS notification is important but it is not more important than the core operation. By writing this in the steering file, we ensured Kiro would implement SNS inside its own try/except block, isolated from the DynamoDB operation.
"A steering file is not just a list of preferences. It is a set of engineering decisions made once and enforced consistently across every code generation in the project."
Describing the Entire Application to Kiro in Plain English
With the infrastructure ready and the steering files updated, we opened Kiro's spec panel and typed this:
Build a Flask web frontend for our Todo app.
The webpage should:
- Show all existing tasks loaded from DynamoDB
- Have a text input box to type a new task title
- Have an Add Task button
- Show each task in a clean card with a red Delete button
- Update the task list instantly without refreshing the page
- Show a success message when a task is added
- Have a clean white design
Flask routes needed:
- GET / → render index.html
- GET /tasks → return all tasks as JSON
- POST /tasks → add task, save to DynamoDB, send SNS
- DELETE /tasks/<task_id> → delete task from DynamoDB
Important:
- Reuse existing db.py — do not rewrite it, only add SNS
- Do not change todo.py at all
- Put all HTML in templates/index.html
- Handle all errors gracefully
Kiro did not write code. It generated three planning documents first.
requirements.md captured every feature we described as a formal requirement with acceptance criteria. design.md laid out the complete architecture — the two-file Flask structure, the template folder, how JavaScript fetch calls would map to Flask routes, how Flask routes would call db.py functions. tasks.md sequenced the implementation in the correct order: Flask setup first, then routes, then the DynamoDB integration, then SNS, then the HTML template, then the JavaScript.
We reviewed all three. Everything was correct. We approved.
Kiro then generated the code.
What Kiro Built — An Honest Walkthrough
I want to be precise here because I think the actual code matters as much as the process. Kiro built three things: an updated db.py with SNS integration, a new app.py with Flask routes, and a new templates/index.html with the webpage.
The Updated db.py — DynamoDB and SNS Together
Kiro added an SNS client alongside the existing DynamoDB connection:
sns = boto3.client("sns", region_name="us-east-1")
TOPIC_ARN = "arn:aws:sns:us-east-1:YOUR_ACCOUNT:TodoAppNotifications"
Notice the difference in boto3 syntax. DynamoDB uses boto3.resource — the higher-level interface that gives you Table objects with methods like put_item and scan. SNS uses boto3.client — the lower-level interface that maps more directly to the underlying AWS API. Both are valid boto3 patterns. The choice depends on which interface is cleaner for each service.
Inside add_task, after saving to DynamoDB, Kiro added the SNS publish call:
sns.publish(
TopicArn=TOPIC_ARN,
Subject="New Task Added — TodoApp",
Message=f"A new task was added: {title}"
)
Three parameters. TopicArn tells SNS which topic to publish to. Subject becomes the email subject line. Message becomes the email body — using an f-string to include the actual task title so the email is specific, not generic.
This SNS call sits inside its own try/except block, separate from the DynamoDB try/except. If SNS fails, the exception is caught and logged. The function still returns a success response. The task is saved. The notification is a best-effort addition, not a critical dependency.
This is called graceful degradation. It is a pattern that distinguishes applications built with production thinking from applications built just to work in demos.
app.py — The Flask Web Server
Four routes. Each one does exactly one thing.
GET / — Renders index.html. When a browser requests the root URL, Flask finds the template in the templates folder and sends it. This is how the webpage appears.
GET /tasks — Calls list_tasks() from db.py, converts the result to JSON with jsonify, returns it. The webpage calls this route when it loads to populate the task list.
POST /tasks — Reads the task title from the request body, validates it is not empty, calls add_task() from db.py (which saves to DynamoDB and triggers SNS), returns a 201 Created response. The 201 status code is semantically correct for a resource creation endpoint.
DELETE /tasks/<task_id> — Receives the task ID from the URL, calls delete_task() from db.py, returns a success response.
What I want to highlight is what is not in app.py. No DynamoDB code. No boto3 imports. No SNS logic. The Flask file handles web concerns — receiving requests, returning responses. db.py handles data concerns. The separation is clean and deliberate.
index.html — The Webpage
The HTML structure is minimal — a heading, a text input, an Add Task button, a div where tasks appear, and a small div for status messages. The CSS keeps everything clean and readable without requiring any external library.
The JavaScript has five functions, and understanding them is worth the time:
• loadTasks() — Calls fetch on /tasks, receives the JSON list of tasks, creates a card element for each one and adds them to the page. Runs automatically when the page loads via window.onload = loadTasks.
• addTask() — Reads the input value, validates it is not empty, calls fetch on /tasks with a POST request and the task title as JSON. On success, calls loadTasks() to refresh the list and shows a success message.
• deleteTask(task_id) — Calls fetch on /tasks/ plus the task ID with a DELETE method. On success, calls loadTasks() to refresh.
• showMessage(text, type) — Displays a status message in green for success or red for error, then hides it after three seconds.
• createTaskCard(task) — Builds the HTML for one task card, including the task title and the Delete button with the task ID attached to its click handler.
No page reloads anywhere. Every interaction happens through fetch calls, and the page updates in place. This is what makes the app feel like a real web application rather than a form that submits and refreshes.
Running It Live — Three Moments That Matter
We ran the Flask server with python app.py and opened localhost:5000 in the browser.
Moment One — Tasks Loading From DynamoDB
The page opened. All the tasks we had added in Part 3 were already there, listed as cards. The webpage had called /tasks, Flask had called list_tasks(), DynamoDB had returned all the items, Flask had returned them as JSON, and JavaScript had turned that JSON into HTML cards. All of this in the time it took the page to load.
For a beginner seeing this for the first time, it lands differently than reading about it. The data was in the cloud. The page found it. That connection — from a web browser to AWS infrastructure — suddenly becomes tangible.
Moment Two — The DynamoDB Console Confirmation
After adding a new task through the webpage, we opened the AWS Console, navigated to DynamoDB, opened the TodoTasks table, and clicked Explore Table Items.
The task was there. With its uuid task_id. With its title. With its pending status. In the cloud.
This confirmation step matters in a tutorial because it removes any doubt about what is actually happening. The data is not in the browser. It is not in Flask's memory. It is in DynamoDB. Seeing it in the console is proof.
Moment Three — The SNS Email
This is the one that makes people stop scrolling.
We switched to the email inbox on screen. An email was there from AWS Notifications. Subject: New Task Added — TodoApp. Body: the exact task title we had typed on the webpage.
I stayed quiet for a few seconds at this point in the video. No narration. Just the inbox on screen. Because the moment needs space to land.
One click on a button in a browser. Flask received the request. DynamoDB saved the task. SNS published a notification. The email arrived. Four distinct steps across three layers of infrastructure, happening automatically, in sequence, in under a second.
"This is what it feels like when cloud services work together. Not as separate tools you configure and manage individually — but as an integrated system that responds to a single user action."
What Building This Actually Taught — Beyond the Tutorial
Separation of Concerns Is Not a Best Practice — It Is a Survival Strategy
Keeping db.py separate from app.py was a decision we made in Part 3. In Part 4, it paid off immediately. We added Flask and SNS without changing a single line of the DynamoDB code. We added a web frontend without touching the CLI. Each piece of the system evolved independently because each piece had a clear, single responsibility.
This is not an advanced concept. It is something every beginner can implement from the first project. And every time you come back to extend the code, you will be grateful you did.
SNS Failure Should Not Kill Your Core Operation
Wrapping the SNS publish call in its own try/except, separate from the DynamoDB operation, is a small thing that represents a significant mindset shift. The notification is useful. But losing a task because SNS had a momentary issue is worse than missing a notification. Understanding which failures are acceptable and which are not — and writing code that reflects that understanding — is what separates applications that work from applications that work reliably.
Steering Files Are Worth Every Minute You Spend on Them
The SNS error handling, the JSON response format, the file structure, the separation of concerns — Kiro implemented all of these correctly because the steering file told it to. We did not have to remind Kiro of our conventions. We did not have to correct generated code that put logic in the wrong file. The upfront investment in clear steering rules paid back immediately and will keep paying back in every subsequent part of the series.
Plain English Specifications Force Clarity
When you have to describe what you want to build in plain English — clearly enough that an AI can generate an accurate specification from it — you discover very quickly whether you actually know what you want to build. Vague descriptions produce vague specifications. Precise descriptions produce precise plans. The discipline of writing a clear specification is valuable entirely apart from what Kiro does with it.
What Comes Next
Part 4 left us with a working full stack web application. A browser. A Flask backend. Two AWS services. All running locally.
Part 5 is deployment. Taking this application from running on a local machine to running live on the internet, accessible from any device, on AWS infrastructure. That means the app will have a real URL, not localhost:5000. It will stay running when you close your laptop. Anyone with the link can use it.
The deployment video covers AWS Elastic Beanstalk for hosting the Flask application, and it will use the same spec-driven workflow with Kiro that we have used throughout the series. Describe the deployment configuration in plain English, Kiro generates the plan, we review and deploy.
If you have been following the series, Part 5 is where everything we have built becomes something you can share with the world.
A Final Thought on What This Series Is Actually About
When I started this series, I made a deliberate choice to build incrementally. Not to show Kiro generating an impressive application in one video. But to build one layer at a time, understanding each layer before adding the next.
Part 1 was about understanding spec-driven development. Part 2 was about building a consistent, automated development environment. Part 3 was about real cloud storage. Part 4 was about real cloud architecture — multiple services, working together, triggered by a single user action.
By Part 4, my viewers are not just watching someone use Kiro. They are understanding Flask routing, DynamoDB operations, SNS publishing, JavaScript fetch calls, and how these pieces connect. They could extend what we built. They could debug it. They could explain it.
That is the goal. Not to show what Kiro can do. But to show what you can build — and understand — when you use Kiro as a tool rather than a shortcut.
"Kiro does not replace your thinking. It replaces your typing. The architecture, the decisions, the understanding — those are yours. Kiro just helps you build it faster and more consistently."
Resources and Links
📺 Part 4 Video — Build a Full Stack Cloud App with Flask + DynamoDB + SNS: https://youtu.be/lRuNiOcqjR8?si=8_NRkngWIWZggSGw
📺 Part 1 Video — Build a Todo CLI App with AWS Kiro: https://youtu.be/LicOYks7l58?si=rW0kghPpx0ZooLHI
📺 Part 2 Video — Hooks and Steering Files Explained: https://youtu.be/hv-8klJ7vLE?si=TmJURkNJDwBfxvFH
📺 Part 3 Video — Connect to AWS DynamoDB: https://youtu.be/T-uIbvwqKOk?si=ilfdywtQw3UD0sAd
🔗 AWS Kiro: https://kiro.dev
🔗 AWS SNS Pricing and Free Tier: https://aws.amazon.com/sns/pricing/
🔗 AWS DynamoDB Free Tier: https://aws.amazon.com/dynamodb/pricing/
🔗 Flask Documentation: https://flask.palletsprojects.com/
🔗 boto3 Documentation: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
🔗 Coding Hives YouTube: https://www.youtube.com/@CodingHives
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article