A product catalog is the worst possible thing to hard-code. The range changes, prices move, photos get replaced — and if every one of those edits needs a developer and a deploy, the site is out of date within weeks and the business stops trusting it. So the brief was less "build a website" than "build something the staff can own".
What I built
I built the public site and its admin dashboard as one React and TypeScript application on Supabase. Visitors get a catalog with product listings and detail pages, services, company information, and an enquiry form. Staff sign in to an admin area covering products with image upload, categories, the enquiries that come in through the site, editable page content, user accounts, and settings — no developer in the loop, no redeploy.
Three tiers of access
Not everyone who edits the site should be able to create logins for it, so I modelled access as three roles rather than a single admin flag: admin, editor, and viewer. The distinction is enforced by row-level security in Postgres, not by hiding buttons — an editor who forged a request would still be refused by the database. Creating and deleting login accounts genuinely needs a privileged key, so I moved that into a Supabase Edge Function that checks the caller is an admin before it acts, which keeps the service key on the server where it belongs.
Finding things
An industrial catalog is only useful if you can find the one part you came for. I used Postgres full-text search rather than filtering in the browser, so the work happens where the data is and search keeps up as the catalog grows. Product images live in their own storage bucket with its own policies, and every query in the app goes through a single typed API layer, so the shape of the data is checked at compile time instead of discovered in production.
What I like about this one is how boring it became to maintain. The catalog stays current because the people who actually know the products are the ones editing it, and the security model means handing someone a login is a low-stakes decision rather than a risk.