Pyzzeria: live order tracking on AWS serverless for $0 idle
Note: the live demo was retired (Nov 2026); the code and this write-up remain.
Problem
I wanted a portfolio demo that a recruiter can use in under two minutes — not a README — and that exercises the parts of AWS I operate every day: API Gateway, Lambda, DynamoDB and Step Functions. The constraints were strict on purpose:
- Real-time feedback: place an order and watch it move through the kitchen states without refreshing;
- $0 while idle: no EC2, no always-on container, no fixed-cost database — everything pay-per-request inside the Free Tier;
- Reproducible: one
sam deploybuilds the whole thing, and the API must be documented (OpenAPI/Swagger) so it can be read without the UI; - Demo only: no real payments, no accounts — the scope is the order lifecycle, not an e-commerce platform.
Approach
Treat the order lifecycle as a state machine and let AWS run it. The API only does two things when an order is placed: persist it in DynamoDB and start a Step Functions Express execution. From there, a chain of Wait → Task states advances the order — recibido → preparando → horno → listo → entregado — and each transition invokes a small Lambda that updates the row and pushes the new status to every browser subscribed to that order over a WebSocket API. The frontend is plain HTML/CSS/JS on S3 + CloudFront; there is no polling. The whole project was built with Spec-Driven Development: proposal, specs, design and tasks live in openspec/ in the public repo.
Architecture
Figure 1 — Request path (solid) and real-time push path (dashed). One SAM template declares all 20+ resources: two DynamoDB tables, four Lambdas, HTTP + WebSocket APIs, the state machine, S3 and CloudFront.
Reading it left to right: the browser posts an order to the HTTP API; the FastAPI Lambda validates it against the menu, stores it in pyzzeria-orders and starts the Express workflow with the orderId. The browser opens a WebSocket and subscribes to that order; the $connect handler stores the connection in pyzzeria-ws-connections with a TTL so abandoned tabs clean themselves up. Every Task state invokes status_update, which writes the new status and pushes it to each live connection through the API Gateway management API — a GoneException deletes stale connections on the fly.
Decisions
D1 · Step Functions Express as the "kitchen clock"
Why: the order lifecycle is a timed sequence, and encoding it as Wait → Task states makes it visible, adjustable and free of sleeping Lambdas. Alternative rejected: a single long-running Lambda with sleep() — it bills for idle time and breaks at the 15-minute limit; or EventBridge Scheduler per transition — more moving parts for the same result. Express (not Standard) because executions are short, high-volume and pay per transition.
D2 · FastAPI behind Mangum instead of hand-written handlers
Why: the same app runs locally with uvicorn and on Lambda unchanged, and FastAPI generates the OpenAPI/Swagger documentation for free — the API is readable without the UI. Trade-off: a slightly bigger cold start than a bare handler; acceptable for a demo where the first request warms it.
D3 · WebSocket push, not polling
Why: polling every second from a browser tab is the cheapest way to leave the Free Tier. API Gateway's WebSocket API keeps the connection, the connections table keeps the mapping, and a status change costs exactly one post_to_connection per subscriber.
D4 · DynamoDB on-demand with two tables and TTL
Why: no instance to pay for, and the access pattern is trivial (get/update by orderId, query connections by order). TTL on the connections table is the garbage collector: no cron, no cleanup Lambda.
D5 · Everything in one SAM template, frontend on S3 + CloudFront with OAC
Why: sam build && sam deploy reproduces the whole stack; the bucket is private and only CloudFront (Origin Access Control) can read it. The frontend is vanilla JS on purpose — it is the same discipline as this portfolio: no build step, no framework, nothing to maintain.
D6 · Scope frozen by the spec
Why: the OpenSpec proposal lists what v1 does not do — authentication, real payments, multiple kitchens, cancellations — so the demo stays honest and finishes. Tests (pytest + moto, ~95% coverage of the backend) cover the menu, the order flow and the Swagger contract.
Result
- Ran live on AWS: build a pizza, place the order and watch it reach
entregadoin about 70 seconds over WebSocket (the public demo was retired in Nov 2026;sam deployfrom the repo brings it back). - $0 idle: no EC2, no RDS, no NAT; all resources are pay-per-request inside the Free Tier.
- Documented and reproducible: OpenAPI/Swagger from the code, one SAM template, SDD artifacts in the repo.
- Reusable pattern: the same "workflow drives a WebSocket feed" shape applies to any long-running job that a user wants to watch.