myself and Marc Pape have been running an e-commerce brand called Smelt for the past couple months, we sell sauna hats. yes its very niche but as someone who sauna’s i knew how hard it was to find a good quality sauna hat in south africa and so began Smelt...
we decided to build the site from scratch instead of paying Shopify $29 every month for the rest of time, it allowed us to creating an incredibly unique design tailored to our exact needs but secondly we encountered some pretty interesting engineering problems along the way as we “scaled” to about 5-6 orders per day which is much more difficult to handle than it sounds as we found out
from the customers pov the site is pretty simple, they’re able to choose a hat, add it to their cart, pay for it and receive some sort of confirmation but little do they know what goes on behind the scenes with stock reservations, payment callbacks, redis scripts and background jobs that somehow stay in sync with eachother
this article is a relatively in-depth look at the “engineering” behind the Smelt ecommerce site - go check it out here:
https://www.saunahat.co.za/
the site is built with next.js & typescript, our persistance layer is literally just a redis store, no relational db, we use paystack for payments and resend to handle emails. given we use many seperate services co-ordinating them gets tricky
the problem with selling the last hat:
lets say that there is a singular green hat left in stock and 2 customers open checkout at the exact same time and they both want to buy that one hat, what happens?
the straightforward solution would just keep a count of the available stock and then check whether there is enough when a user’s adds it to cart, and then decrement the count.
const available = await getStock("green");
if (available >= quantity) {
await setStock("green", available - quantity);
return { reserved: true };
}problem being is that the request to get the available stock does not stop other requests from running simultaneously, think of this situation

in this case we have accepted 2 orders for 1 hat which is kind of an issue, initially i thought we could just atomically decrement the counter but that did not fix it since both requests could still pass the earlier check and then decrement the counter to 0 and then -1. we needed to co-ordinate the check and deduction together. we did this using a Lua script executed inside Redis.
stock reservations:
instead of us sending a seperate read and then write request to Redis we send a single request telling Redis to run the reservation script which reads the counters, checks the request and does the decrement. this script is run without executing another clients command halfway through it and we end up with this result:
Customer A:
read 1 → reserve 1 → write 0 → finish
Customer B:
read 0 → reject reservation → finishnext we have the concept of a reservation which is a stock allocation with an associated purchase.
type ReservationStatus = "held | "paid" | "released | "expired";held means we have reserved a hat for someone but they havent paid yet, paid is means we have an actual payment corressponding to that reservation, released is when payment fails and expired is when the expired state is reached after a certain amount of inactivity like an abandoned cart.
suppose someone adds 2 green hats to their cart and 5 are in currently in stock, in redis we create a reservation that looks like this:
Reservation:
reference: "purchase-123"
green: 2
cream: 0
status: "held"
createdAt: <creation time>currently the hats have been set aside but there has been no payment committed yet so customers see 3 green hats available since 2 are now reserved.
we then try to create a payment with Paystack, if creation times out we keep the reservation because Paystack may have created the payment even though we never received its response. the inventory worker later checks Paystack before deciding whether to keep or release the allocation.
payment webhooks:
once the payment is successful we use Paystack’s webhook to learn about the payment. Paystack sends an POST request to Smelt’s /api/paystack/webhook endpoint when a payment succeeds:
POST /api/paystack/webhook HTTP/1.1
Host: www.saunahat.co.za
Content-Type: application/json
x-paystack-signature: <signature calculated by Paystack>
{
"event": "charge.success",
"data": {
"reference": "smeltp-example-purchase",
"status": "success",
"amount": 900,
"currency": "ZAR",
"paid_at": "2026-10-18T10:30:00.000Z",
"customer": {
"email": "customer@example.com"
},
"cart": {
"green": 1,
"cream": 0
}
}
}
}whats interesting is that the webhook endpoint is publicly reachable, so receiving a request that says “payment successful” is not sufficient. we calculate an HMAC-SHA512 signature over the raw request body using our Paystack secret and compare it with the signature in the request header, once that is all good we can move on to more checks...
we obviously can’t trust the browser to choose what the correct price was so the server calculates the total based on the cart and shipping and then its calculated again based on Paystack’s response - these amounts are then compared:
const expected = paidCheckoutTotal(
cart,
shippingMethod,
discount,
unitPrice,
pricingVersion,
);
const matches =
expected > 0 &&
payment.amount === expected &&
payment.currency === "ZAR";if those all succeed we find the reservation in Redis and set its status = “paid”:
Reservation:
reference: "purchase-123"
green: 2
cream: 0
status: "paid"
createdAt: <creation time>cleaning up the abandoned carts
in the (very unfortunate for us) situation that a customer abandons their cart we do eventually need to release their reserved stock, after an hour a reservation becomes eligible for garbage collection.
we have a worker route at /api/cron/inventory. Vercel Cron is configured to call it every five minutes. each time its called it examines held reservations that are at least an hour old and follows the actions based on this table to garbage collect old reservations:
once the payment is settled i know marc and i are happy but unfortunately the technical battle is not over yet, we need to send a confirmation email to let the customer know we have received their order
sending emails exactly once:
this is simple, we tell resend to send this email, it sends it and we are done but not quite, think of this scenario:
1. app -> resend: send this email
2. resend accepts the request
* connection drops before response reaches app
3. app sees timeoutsee in this situation do you retry? you technically don’t know whether the email failed to deliver or just the acknowledgement failed - we definitely don’t want to just blindly try again and risk spamming customers with confirmation emails.
we handle this by storing a pending receipt before sending which contains the message and the time the attempt started and using an idempotency key that we create from the type of notification (sms, email etc.) and the payment reference:
await resend.emails.send(receipt.message, {
idempotencyKey: `family/{hash}`,
});we use the receipt to remember the content of the email we wanted to send in case the operation to write the updated status of the email to redis fails and the worker that checks for unsent emails needs to resend it. resend does the de-duping for us using the idempotency key.
the ai chatbot:
lastly we have this pretty cool ai chat integration on the site where customers can ask ai anything about the product and it’ll try it best to answer them:
for each question we construct some reference info for the model to use such as the products, the stock availability, current pricing, policies and a few other things, the model then receives that info along with the conversation, the model is asked (begged using a system prompt) for a structured response like this:
{
answer: string;
sources: string[];
needsHuman: boolean;
}since we don’t want users absolutely destroying our limited ai budget we have per visitor per day limits which we store in Redis for each user which gets decremented for each question they ask, currently we have the limit at 10 questions per user per day which is pretty fair, we use the gpt-5.6-luna model which is very affordable.
five or six orders a day is obviously not a throughput problem. but it is enough to encounter some correctness problems that have been pretty interesting to solve
was building an entire e-commerce platform to save $29 a month financially sensible? probably not. but we wanted complete control over the experience, and it gave us an excuse to solve some interesting problems.




When the ai bot needs a human or can’t respond to question, how does that process work? Does it say it it referring to a human and send you guys a message or how would one integrate this into there website?
Loved this stack, definitely gonna check out your website ! Pay need to get a sauna hat for my sauna experience