I built Fasco as a real fashion storefront, not a demo. This post is about the half customers actually touch: the shop, the cart, and checkout. The admin panel has its own writeup.
You can click around the live build here: https://fasco-sooty.vercel.app/
Why I make you sign in before adding to cart
Most tutorials let anyone fill a cart and only ask for an account at checkout. I went the other way. On Fasco you sign in before you can add anything or save a favorite.
The reason was practical. The cart isn't local state I throw away on refresh. It lives in Supabase, tied to your account, so it's still there when you come back tomorrow on a different device. That only works if I know who you are up front. The cost is a little more friction early on. Since I also wanted favorites and abandoned-cart recovery to work, it was worth it.
The shop page and filtering
The shop page filters on size, price range, category, and rating. Nothing exotic, but I spent real time making the filters feel instant instead of triggering a full reload on every click. React and Vite did most of the heavy lifting, and the dev loop was fast enough that I actually enjoyed tuning this part.
Light and dark mode are both there. I default to the system setting and let people override it.
Checkout: Stripe and Paystack
Fasco takes payments through two providers. Stripe handles international cards, Paystack handles local ones. Picking the right gateway for the buyer turned out to be fiddlier than either integration on its own.
The part I'm proud of is how an order becomes "Paid." It never happens in the browser. The frontend starts the payment, but the order status in the database stays "pending" until the payment provider sends a webhook back to my server confirming the money actually moved.
If you mark an order paid from the client, you're trusting the browser. Don't.
Roughly, the flow looks like this:
// client starts checkout; the order is created as "pending"
const order = await createOrder({ items, status: "pending" })
// later, on the server: Stripe/Paystack calls this webhook
export async function handleWebhook(event) {
if (event.type === "payment.success") {
await supabase
.from("orders")
.update({ status: "paid" })
.eq("id", event.metadata.orderId)
}
}That one rule, only the webhook can flip an order to paid, killed a whole class of fake orders before they could exist.
Supabase doing the unglamorous work
Two things made Supabase the right call. First, row-level security: a policy makes sure you only ever read your own orders, while an admin can read everything. I'm not filtering by user in the frontend and hoping the query is right. The database refuses to hand over rows that aren't yours. Second, persistence: cart contents, quantities, and the running total with tax and shipping all survive a refresh, because they live server-side instead of in component state.
What I'd tell past me
Build the payment-confirmation flow first, before any of the pretty parts. I wired up the UI early and then had to bend it around the webhook reality later. Start from "how do I know this money is real," and the rest of checkout falls into place.


