Skip to content
2026 · Full-stack development

Crown Kidz

A kidswear storefront with guest checkout and an admin panel.

Next.js 16 (App Router)TypeScript (strict)Tailwind CSS 4Postgres (Neon)PrismaBetter AuthRazorpayCloudinaryZodVercel

I built Crown Kidz as a kidswear storefront backed by a catalogue and order-management system. Shoppers can browse products and collections, choose size and colour variants, add items to a cart, and check out without creating an account.

The admin panel manages products, collections, images, variant prices and stock, and order status. Better Auth supplies staff sessions, Cloudinary handles uploaded product images, and Prisma connects the application to Postgres on Neon.

The checkout and payment logic lives in server-only modules. It re-prices carts from the catalogue, reserves stock while payment is pending, creates Razorpay payment orders, and handles browser confirmations, signed webhooks, cancellations and expired reservations.

The browser doesn't get to decide what things cost

The browser sends variant IDs, quantities and the price the shopper was shown. That price is a snapshot for detecting a stale cart. The server looks up the variant and its product, applies any variant price override, and calculates the subtotal, shipping and total from database values.

If the displayed price no longer matches, checkout rejects the affected line instead of silently charging a different amount. Duplicate lines for the same variant are combined before quantity and stock checks, and missing or inactive products are rejected. Zod validates the submitted cart and delivery details.

Prices and order totals are stored in integer paise. Order items snapshot the purchased name, variant and price so later catalogue edits do not rewrite the order history.

Two buyers, one dress left

A stock check during cart pricing is only advisory: another checkout can reach the same item before the order is written. When placing the order, each stock decrement is conditional on the variant still having enough units. If an update affects no rows, the transaction rolls back the earlier decrements and the order creation.

Stock is reserved when the order becomes pending, before payment. Each reservation has a payment deadline. Cancellation and the expiry sweep change the order state and return stock in one transaction, with a conditional state update so repeated cancellations do not return the same units twice.

Checkout processes variants in a consistent ID order. The code includes a scheduled expiry endpoint and a manual admin release action; the payment window defaults to 30 minutes, while the Vercel sweep is configured to run daily.

The browser callback and webhook can report the same payment

The payment flow has two confirmation paths: the browser callback and the Razorpay webhook. The webhook verifies an HMAC over the raw request body before parsing it. The browser callback verifies its payment signature and checks that the payment belongs to the order whose access token the caller holds.

Both successful paths call the same settlement function. It conditionally marks the payment captured and the pending order paid inside a database transaction, clearing the expiry deadline. Repeated reports leave an already-settled payment unchanged. A failed-payment webhook also checks the current state before writing, so it does not overwrite a captured payment.

Signature comparisons use timingSafeEqual. Retry handling is based on payment and order state; there is no separate webhook event-ID ledger.

Staff access is checked where the data changes

Admin pages and mutation actions use a shared requireAdmin guard that reads the Better Auth session and requires the admin role. Product, image, collection and order actions call it before changing data, rather than relying only on the navigation or dashboard layout.

Order actions define the allowed status transitions: pending to paid or cancelled, paid to shipped or cancelled, and shipped to delivered. Cancellation goes through the shared stock-return function. Guest order pages use a random access token, so shoppers can view their order without a customer account.

What I’d do differently

  • 01I'd add integration tests around concurrent checkouts, duplicate payment reports and payment arriving near a reservation's expiry. Conditional database updates are visible in the implementation, but the repository has no automated tests proving these scenarios.
  • 02I'd align the expiry sweep frequency with the payment window. A 30-minute deadline and a daily scheduled sweep mean an abandoned order can hold stock beyond its deadline until the sweep or a manual release runs.