How I Built and Deployed a Full-Stack Todo App in Under 5 Minutes Using Waft.dev
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.
Comments
Be the first to comment on this post.