← All projects

VKT Bougie

A storefront that refuses to price anything itself.

Visit VKT Bougie

VKT Bougie screenshot
Tests
621
Working facets
8
States covered
37
Since
2023

The problem

A storefront and its commerce backend are two systems that must agree on one number: what the customer owes. If both compute it, they will eventually disagree, and the disagreement surfaces after the card has been charged.

The second problem is Nigerian delivery. Fees are set per state and per area rather than by distance, some options are pickup with a refundable deposit rather than a fee, and the same state answers to several spellings.

What I built

A SvelteKit 2 storefront on Svelte 5 runes, deployed to Vercel. Catalog reads go straight to the commerce database through a SELECT-only role, kept in a server-only module so the client bundle cannot reach it; orders, customers, wishlists and reviews go over an HTTP API authenticated by a per-space storefront key.

Faceted search is server-side SQL, not client-side filtering: text, category, colour, size, material, price range and sale status, with four sorts and pagination. Variant selection has its own algorithm and its own test file, because picking a size should not silently revert the colour you already chose.

Delivery is a state and option picker over the merchant’s real zone table, with the fee and any refundable deposit kept as separate quantities.

Decisions I'd defend

The storefront never computes what it charges. It takes the total from the backend verbatim, because a second implementation here would be a second chance to disagree. What it does do is assert that the server’s own figures sum to the server’s own total within half a kobo, and refuse to open the payment popup if they do not. That check caught a real near-miss where a pickup deposit was added to the quote breakdown: had the total excluded it, checkout would have charged short and the order would have been rejected after the debit.

The payment key is bound to the deploy environment. A live key on staging and a test key in production are both refused, with shopper-facing copy rather than a silent charge, because neither the key nor the environment knows which one the other landed in.

Facet values are derived from the whole catalogue rather than the current page: a colour swatch should exist because some product somewhere is that colour, not because it happens to be on page three.

Nigerian states are normalised against a canonical list of the thirty-six plus the FCT, written because the merchant’s own table spelled Nasarawa two ways and the FCT four. Unrecognised states are logged and dropped rather than guessed at, and there is deliberately no catch-all delivery fee: the previous fallback was deleted precisely so one could not be reintroduced by accident.

Anonymous responses are cacheable, signed-in ones are not, and the guard is the conditional rather than a Vary header, because a public header on a signed-in data payload would put a customer email into a shared cache.

What went wrong

The catalogue used to be filtered in the browser: the page requested five hundred products and narrowed them client-side, which was quietly broken because the API clamps its page size to a hundred. Filtering, sorting and pagination moved to the server.

It also started life with no test coverage on the checkout path, no error monitoring and per-instance rate limiting. All three were addressed, though the rate limiter still fails open without its shared store, and I would rather say that than imply otherwise.

Where it is now

Live, with 621 tests and continuous integration that gates lint, a custom Svelte template rule, type checking, tests and a full build on every pull request.

The homepage hero is still hardcoded rather than merchant-managed, and the instant-indexing path is plumbed on this side but has nothing calling it yet. Both are known, and both are on the list.