All projects

Preety Flower Shop

2026

A full-stack flower shop built for a Kathmandu-based business, with a customer storefront, a WhatsApp-backed checkout, and an admin portal running on AWS serverless infrastructure. Deployed and functional, though the shop never adopted it as their day-to-day system.

Next.jsTypeScriptTailwindshadcn/uiAWS LambdaAPI GatewayDynamoDBS3 + CloudFrontCognitoSST
Preety Flower Shop

The problem

Preety Flower Shop is a real flower business in Naxal, Kathmandu. Like most small Nepali shops, it took orders over the phone and on WhatsApp, which worked until it didn't: no catalog for customers to browse, prices quoted one at a time, and every order depended on remembering a conversation. The owner needed a storefront customers could browse on their own, plus a simple way to see and manage what came in.

There was one hard constraint from the start: no payment gateway. Online payment adoption for small businesses in Nepal is spotty, and the owner already trusted a Fonepay QR code and cash on delivery. So the checkout had to feel complete without ever touching money.

Architecture

The site is a pnpm workspace with three pieces: a Next.js app (apps/web) holding both the public storefront and the admin portal, a shared package (packages/core) with the DynamoDB repositories and Zod schemas, and a UI package of shadcn components. All infrastructure is declared with SST v3: an API Gateway REST API fronting Lambda handlers, a single-table DynamoDB, a private S3 bucket served only through CloudFront, and a Cognito user pool for the admin.

The API is deliberately thin. Each Lambda handler parses the request with a Zod schema before touching the database, resolves product prices server-side so the client can never inflate or undercut a total, and writes the order through a shared core repository. Routes are split between public ones (products, categories, orders) and admin ones (orders, products, categories, presigned image uploads), with the admin routes protected by a Cognito JWT authorizer that rejects unauthenticated requests before any handler code runs.

DynamoDB uses a single-table design with PK/SK prefixes (PRODUCT#, CATEGORY#, ORDER#) and two GSIs, one listing products by category and one grouping orders by status. All timestamps are stored in UTC and rendered in NPT (UTC+5:45) on the frontend, a small detail that matters when every order deadline is a same-day delivery.

Checkout without a payment gateway

The interesting constraint shaped the entire order flow. There is no payment step. When a customer checks out, the API creates the order with status PENDING_CONFIRMATION and returns the order id and total. The frontend then hands the customer two things: a WhatsApp deep link prefilled with their order details and the shop's Fonepay QR code, and a fallback cash-on-delivery option. The owner confirms the order in the admin portal, which flips the status and starts the fulfillment lifecycle.

Keeping money off the platform also kept the trust surface small. The shop never holds card data, never reconciles payments, and the only payout path is one the owner already uses daily. The order total is still computed and stored server-side, so the WhatsApp message and the admin view always agree on the price.

The admin portal

The admin side lives in the same Next.js app under route groups, with a login flow against the Cognito pool and a JWT passed to the API. It handles products, categories, and orders, with a status workflow that moves an order from PENDING_CONFIRMATION through CONFIRMED, PREPARING, READY, and DELIVERED (or CANCELLED).

Image uploads use presigned S3 URLs: the admin asks the API for a short-lived URL scoped to the uploads/ prefix, the browser PUTs the image directly to S3, and CloudFront serves it with a year-long cache TTL. The bucket stays fully private throughout, so the only public surface is the CDN.

Cost and ops from day one

The whole backend is designed around a small-business budget. Lambdas run on ARM64 (Graviton) with 256 MB and a 10-second timeout, every function gets a reserved concurrency of 10, and DynamoDB is on-demand billing. CloudWatch alarms and an AWS Budget are wired to an SNS topic that emails the owner: Lambda error rate, invocation spikes, API Gateway 5xx, DynamoDB throttling, and spend alerts at $10 and $25 a month. The escalation playbook is written down, starting with setting reserved concurrency to 0 as the kill switch.

SST makes the whole stack redeployable from a config file, and the security posture is documented as code: no public S3 access, least-privilege IAM per function, throttling and WAF on the API, short token expiry and MFA on the admin pool. For a project built for a real business, that boundary between 'it works' and 'it is safe and cheap to run' was the part worth doing properly.

Outcome

Being honest about where this landed: the site was built, deployed, and fully functional, but the shop never actually adopted it. Orders kept flowing through the owner's phone and WhatsApp as before, and the admin portal never became their daily workflow. So today the live site works but reads like a demo, stocked with sample products rather than the shop's real catalog.

That is the most instructive part of the project. Building for a real business exposed the gap between what a developer counts as done (everything works, every flow is tested) and what makes a small shop switch to new software (a daily habit they'd have to change, setup someone has to do, a reason the old way is actually painful). The product was not the failure. The adoption plan was the missing piece, and I would start there next time: define who maintains the catalog, seed it with real inventory, and hand the owner a reason to open the admin panel on day one.