A bakery storefront and store-management system with transactional ordering in Postgres, role-based administration and verified PayHere payments in LKR.
Project media coming soon
Screenshots and demo videos for this case study will appear here.
Royal Bakery combines the public storefront, customer accounts and staff tools in one Next.js App Router client, backed by a typed Express API and Supabase. Customers browse the catalogue, manage a server-side cart, place orders and pay with PayHere. Admins manage categories, products, images, orders and enquiries.
Commerce rules had to hold even when requests raced each other. Two customers buying the last item, a cart changing mid-checkout, or a forged payment callback could each leave orders, stock and payments inconsistent.
I used three tiers. Next.js handles presentation, an Express and TypeScript REST API owns business rules, and Supabase provides Postgres, Auth and Storage. Order placement runs inside a Postgres function, so it is all-or-nothing. PayHere checkouts are signed on the server and payment notifications are verified before any state changes. I designed each slice in a written spec first and covered it with Vitest tests.
The result is a complete commerce system with clearly separated layers, database-enforced consistency and a payment flow built for Sri Lankan transactions.
Separate route groups for the shop, authentication and admin, with server actions where they simplify forms.
Routers for catalogue, cart, orders, payments, users and admin. Supabase JWTs are verified in middleware, and requireRole('admin') guards the back office.
Versioned migrations for the schema, a trigger that creates a profile row on signup, row-level security, and SQL functions for placing and cancelling orders.
Product images go to a storage bucket with sanitised paths. PayHere checkout is MD5-signed on the server, and notifications are verified before order state changes.
create_order_from_cart runs as one Postgres function. It locks the product rows (FOR UPDATE), checks availability and stock, prices every line from the database rather than from client input, writes the order and its items, decrements stock and clears the cart in a single transaction. Concurrent checkouts can't oversell, and a client can't change a price.
Row-level security is enabled on every table, so the public anon key can't read or write business data on its own. All access goes through the API, which applies authentication and role checks before using a server-only service key.
The browser's return from PayHere never marks an order as paid. Only the server-to-server notification can, after its md5sig is recomputed with the merchant secret and matched. Status codes then map to explicit payment and order states: paid, failed or still pending.