Pempek Palembang Cek Lis
A storefront and custom CMS for a pempek shop in Serpong, South Tangerang. The owner manages the menu, promo banners, reviews, and contact details from an admin panel instead of asking me to change the code.
The problem
The shop had no website. Customers found it through Instagram and had to ask for the menu and prices in DMs, one conversation at a time. Anything I hard-coded would have meant the owner messaging me every time a price changed, so a static site was not a real option.
What I set out to build
Give the shop a page it can send to customers that always shows current prices, and make sure every piece of content on it can be edited by the owner without touching the code or waiting for a deploy.
Approach
A Next.js App Router site reading content from Postgres on Supabase. Pages are statically generated and revalidated, so the public site stays fast but picks up admin edits without a rebuild. Orders skip a custom cart entirely: each product links to WhatsApp with a pre-filled message, which is how the shop already took orders.
Overview
A public storefront plus an admin panel for a local pempek business. I built it unpaid for a family friend's shop, which made it the first thing I have written that someone other than me depends on. The public side shows the menu with prices, promo banners, customer reviews, delivery areas, and shop hours, and sends orders to WhatsApp with the product already filled into the message. The admin side covers the menu, banners, reviews, and site settings such as the WhatsApp number and social links.
Architecture
Turborepo monorepo with pnpm workspaces holding the public site and the admin panel. Supabase Postgres for content, with Row-Level Security so the anonymous client can read published rows but only an authenticated admin can write. Supabase Auth for admin login, Supabase Storage and Firebase for product and banner images. Deployed on Vercel.
What shipped
- Live at pempekceklis.biz.id with a 6-item menu, 3 promo banners, 4 customer reviews, and 5 delivery areas, all of it stored in Postgres and editable from the admin panel.
- The owner can change a price, swap a banner, or update the WhatsApp number without a code change or a redeploy.
- Orders open WhatsApp with the product already written into the message, so there is no order form or cart to maintain.
Hard parts & trade-offs
- Getting the Row-Level Security policies right so the public site could read the catalogue with the anon key while writes stayed locked to the admin session. The first version leaked write access through a policy that only checked authentication, not role.
- Choosing revalidation windows per content type. Prices need to update quickly; the about text does not. Revalidating everything aggressively removed the point of static generation.
- Two image backends (Supabase Storage and Firebase) is more moving parts than this project needed. It works, but if I rebuilt it today I would pick one.
What I learned
- Row-Level Security is worth the setup cost, because the access rule lives next to the data instead of being re-implemented in every query.
- The owner did not want features, they wanted to stop answering the same DM. Reading the request that way changed what I built.
- A monorepo made sharing types between the public site and the admin panel easy, but it is more tooling than a two-app project strictly needs.
What's next
- 1Replace the placeholder WhatsApp number and the leftover test banner with real data.
- 2Consolidate image storage onto Supabase and drop the Firebase dependency.
- 3Add Indonesian and English versions of the menu descriptions.