Log In Sign Up
March 27, 2026 Tutorials & How-To

How I Built and Deployed a Full-Stack Todo App in Under 5 Minutes Using Waft.dev

RookyNex 7 min read

My mate Darren Oakey recently built waft.dev, a platform that lets AI agents deploy applications to production with zero infrastructure configuration. I decided to put it through its paces by building a complete todo list application with authentication and deploying it using claude – all from the command line.

Here’s exactly how I did it, step by step.

What We’re Building

A fully functional todo list web app with:

  • User registration and login (JWT-based auth)
  • Create, read, update, and delete todos
  • Mark todos complete/incomplete
  • Filter by All / Active / Completed
  • Dark-themed responsive UI
  • Deployed live at https://todo-app.waft.dev

The entire app is a single Python file — no frameworks, no dependencies, no Docker, no YAML.

Prerequisites

  • A waft.dev account (Starter plan or above)
  • An API token (generate one from Profile in the waft portal)
  • Python 3.12 installed locally (for testing)
  • curl

Step 1: Understand How Waft Works

Waft deploys your code as an AWS Lambda function behind a managed API Gateway. It auto-detects your runtime based on what’s in your zip file:

File Present

Runtime

bootstrap

Go / Rust (provided.al2023)

index.js

Node.js 20.x

handler.py (or anything else)

Python 3.12

Your entry point is a handler(event, context) function — the standard AWS Lambda signature. Waft handles SSL, DNS, scaling, and gives you a https://<app-name>.waft.dev URL automatically.

No Dockerfiles. No Terraform. No CloudFormation. Just code.

Step 2: Write the Application

I created a single file: handler.py. It contains everything — the API backend, authentication logic, and the entire frontend (HTML/CSS/JS embedded as a Python string).

Project structure

todo-app/
handler.py # That’s it. One file.

The Lambda handler (router)

The core is a routing function that maps HTTP methods and paths to handler functions:

def handler(event, context):
method = event.get(“httpMethod”) or event.get(“requestContext”, {}).get(“http”, {}).get(“method”, “GET”)
path = event.get(“path”) or event.get(“rawPath”) or “/”

if method == “OPTIONS”:
return json_response(204, “”)

if method == “GET” and path == “/”:
return html_response(FRONTEND_HTML)

if method == “POST” and path == “/api/register”:
return handle_register(event)
if method == “POST” and path == “/api/login”:
return handle_login(event)

if path == “/api/todos”:
if method == “GET”:
return handle_list_todos(event)
if method == “POST”:
return handle_create_todo(event)

# … more routes

return json_response(404, {“error”: “Not found”})

Authentication (zero dependencies)

I implemented JWT tokens using only Python’s standard library — no PyJWT, no external packages:

import hashlib, hmac, base64, json, time

JWT_SECRET = “your-secret-key”

def create_token(email):
header = b64url_encode(json.dumps({“alg”: “HS256”, “typ”: “JWT”}).encode())
payload = b64url_encode(json.dumps({
“email”: email,
“exp”: int(time.time()) + 86400
}).encode())
signature_input = f”{header}.{payload}”.encode()
signature = b64url_encode(
hmac.new(JWT_SECRET.encode(), signature_input, hashlib.sha256).digest()
)
return f”{header}.{payload}.{signature}”

Passwords are hashed with PBKDF2 (100,000 iterations), also from the standard library:

def hash_password(password, salt=None):
if salt is None:
salt = uuid.uuid4().hex
hashed = hashlib.pbkdf2_hmac(“sha256”, password.encode(), salt.encode(), 100000)
return hashed.hex(), salt

The API endpoints

Seven endpoints cover everything:

Method

Path

Auth

Purpose

POST

/api/register

No

Create account

POST

/api/login

No

Login, get JWT

GET

/api/me

Yes

Current user info

GET

/api/todos

Yes

List todos (with filter)

POST

/api/todos

Yes

Create a todo

PATCH

/api/todos/{id}

Yes

Update a todo

DELETE

/api/todos/{id}

Yes

Delete a todo

The frontend

The entire UI is a string literal inside handler.py, served at GET /. It’s a single-page app with vanilla JavaScript — no React, no build step:

FRONTEND_HTML = “””<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<title>Todo App</title>
<style>
/* Dark theme CSS */
</style>
</head>
<body>
<!– Login/Register form –>
<!– Todo list UI –>
<script>
// Vanilla JS: auth, CRUD, filtering
</script>
</body>
</html>”””

The JS talks to the API on the same origin, stores the JWT in localStorage, and handles all CRUD operations with fetch().

Step 3: Test Locally

Before deploying, I tested the handler locally by calling it directly with Python:

import handler, json

# Test the frontend
result = handler.handler({“httpMethod”: “GET”, “path”: “/”}, None)
assert result[“statusCode”] == 200

# Test registration
result = handler.handler({
“httpMethod”: “POST”,
“path”: “/api/register”,
“body”: json.dumps({“email”: “test@test.com”, “password”: “password123”})
}, None)
assert result[“statusCode”] == 201
token = json.loads(result[“body”])[“token”]

# Test creating a todo
result = handler.handler({
“httpMethod”: “POST”,
“path”: “/api/todos”,
“headers”: {“Authorization”: “Bearer ” + token},
“body”: json.dumps({“title”: “Buy groceries”})
}, None)
assert result[“statusCode”] == 201

All endpoints. No server needed. Just import and call.

Step 4: Deploy to Waft

This is where it gets fun. Three API calls and we’re live.

4a. Verify your token

curl -s -H “Authorization: Bearer $WAFT_TOKEN” \
“https://waft.dev/api/users/me”

{
“email”: “you@example.com”,
“plan”: “starter”,
“is_active”: true
}

4b. Create the app

curl -s -X POST “https://waft.dev/api/apps” \
-H “Authorization: Bearer $WAFT_TOKEN” \
-H “Content-Type: application/json” \
-d ‘{
“name”: “Todo App”,
“slug”: “todo-app”,
“description”: “A todo list with auth”,
“runtime”: “lambda”
}’

Response:

{
“id”: “c060c05b-1057-4dd8-a199-e14c2dbd13ef”,
“slug”: “todo-app”,
“url”: “https://todo-app.waft.dev”,
“runtime”: “lambda”,
“is_active”: true
}

App created. URL reserved. No infrastructure provisioned yet — that happens on first deploy.

4c. Zip and deploy

zip -j /tmp/todo-app.zip handler.py

curl -s -X POST \
“https://waft.dev/api/apps/c060c05b-…/deployments/upload” \
-H “Authorization: Bearer $WAFT_TOKEN” \
-F “file=@/tmp/todo-app.zip”

Response:

{
“version”: 1,
“status”: “live”,
“build_log”: “Created Lambda function: waft-e97e9498-todo-app”,
“deploy_log”: “Deployment successful”,
“created_at”: “2026-03-12T04:11:27.717129”,
“completed_at”: “2026-03-12T04:11:48.146912”
}

20 seconds. From zip upload to live URL. Waft created the Lambda function, configured API Gateway, provisioned SSL, and set up DNS — all automatically.

Step 5: Verify in Production

With the app live, I ran a full integration test against the production URL:

# Health check
curl -s “https://todo-app.waft.dev/api/health”
# {“status”: “ok”}

# Register a user
curl -s -X POST “https://todo-app.waft.dev/api/register” \
-H “Content-Type: application/json” \
-d ‘{“email”:”user@test.com”,”password”:”secure123″}’
# {“token”: “eyJ…”, “email”: “user@test.com”}

# Create a todo
curl -s -X POST “https://todo-app.waft.dev/api/todos” \
-H “Authorization: Bearer $TOKEN” \
-H “Content-Type: application/json” \
-d ‘{“title”:”Buy groceries”,”description”:”Milk, eggs, bread”}’
# {“id”: “5e6a3f6f752b”, “title”: “Buy groceries”, “completed”: false}

All 16 tests passed: registration, login, CRUD, filtering, auth guards, error handling.

Step 6: Fix a Bug, Redeploy in Seconds

I discovered a JavaScript syntax error in the frontend — single quotes inside onclick attributes were getting mangled by Python’s string escaping. The sign-up form wasn’t working in the browser even though the API was fine.

The fix was straightforward. Then redeploying was just:

zip -j /tmp/todo-app-v2.zip handler.py

curl -s -X POST \
“https://waft.dev/api/apps/$APP_ID/deployments/upload” \
-H “Authorization: Bearer $WAFT_TOKEN” \
-F “file=@/tmp/todo-app-v2.zip”

{
“version”: 2,
“status”: “live”,
“completed_at”: “2026-03-12T04:55:14.446053”
}

2 seconds for the update. The Lambda function was already created, so it just swapped the code.

Step 7: Monitor with Waft’s Built-in Telemetry

Waft provides observability out of the box. No Datadog or CloudWatch setup needed:

# Request summary
curl -s -H “Authorization: Bearer $WAFT_TOKEN” \
“https://waft.dev/api/apps/$APP_ID/telemetry/summary?hours=1”

# Application logs
curl -s -H “Authorization: Bearer $WAFT_TOKEN” \
“https://waft.dev/api/apps/$APP_ID/logs?hours=1&limit=20”

# Path-level analytics
curl -s -H “Authorization: Bearer $WAFT_TOKEN” \
“https://waft.dev/api/apps/$APP_ID/telemetry/paths?hours=1”

You can see request counts, error rates, latency percentiles, and per-path breakdowns — all through the API or the portal dashboard.

What Waft Gave Me for Free

Here’s what I didn’t have to do:

  • Write a Dockerfile
  • Create a serverless.yml or template.yaml
  • Configure API Gateway
  • Set up SSL certificates
  • Manage DNS records
  • Create IAM roles
  • Configure CloudWatch
  • Set up a CI/CD pipeline
  • Touch the AWS console at all

Waft handled all of it. I wrote code, zipped it, uploaded it, and got a live HTTPS URL.

The Full Waft Deployment API

For reference, here’s the complete deployment flow:

1. POST /api/apps → Create app
2. POST /api/apps/{id}/deployments/upload → Deploy code
3. GET /api/apps/{id}/deployments/{dep_id} → Check status
4. GET /api/apps/{id}/telemetry/summary → Monitor
5. GET /api/apps/{id}/logs → Debug
6. POST /api/apps/{id}/security → Add rate limiting, WAF, etc.

You can also add custom domains, enable security modules (rate limiting, WAF, CORS, API key auth), set up GitHub Actions CI/CD, and roll back to any previous deployment — all through the API or via natural language if you’re using the MCP plugin with Claude Code, Gemini CLI, or Codex.

Pricing

Waft offers four tiers:

Plan

Price

Apps

Requests/mo

BYOC

$9/mo

5

100K

Starter

$29/mo

5

100K

Pro

$79/mo

25

1M

Business

$199/mo

Unlimited

10M

For a side project or proof of concept, the Starter plan is more than enough.

Final Thoughts

Waft removes the gap between “my code works locally” and “my code is live in production.” The entire deploy cycle — create app, upload zip, live URL — takes under 30 seconds. Redeployments take 2 seconds.

The key insight is that Waft is designed for AI agents, but it works just as well for humans. The API is clean, the responses are predictable, and you don’t need to understand AWS to deploy to AWS.

If you’re building prototypes, side projects, or even production services that fit the serverless model, Waft is worth trying. I went from zero to a deployed full-stack app in under 5 minutes, and most of that time was writing the code — not fighting infrastructure.

Try it: waft.dev

The complete source code for this todo app is a single handler.py file — approximately 500 lines of Python with an embedded frontend. No dependencies. No configuration. Just code and a zip file.

Share

Comments

Be the first to comment on this post.

Leave a Comment