Success Story: Fully headless ecommerce on Frappe: ERPNext + Next.js 16 + Meilisearch

Hi everyone,

Wanted to share a production story from Bangladesh. We run M.K. Electronics (https://mke.com.bd), an electronics retailer that has been around since 1983, with 16 physical superstores. Over the last year we rebuilt the whole online presence as a headless storefront on top of ERPNext. It has been live for months, and since this forum has helped me plenty over the years, here is how it works and what we learned.

The one-line pitch: the same ERPNext that runs our POS, stock and accounting also serves the storefront. There is no sync job between the shop and the books. A price change in the back office is on the website seconds later, cache and all.

Why not the built-in webshop

We needed full control of the frontend: design, SEO, Core Web Vitals, and pages served from CDN edge. That is hard to get from server-rendered website templates. So the storefront is a separate Next.js 16 app, and ERPNext stays what it is best at, the system of record.

The architecture

A custom Frappe app (we call it ecom) exposes a whitelisted REST API: route resolution, product data, carts, OTP login, payments. In front of it:

  1. Next.js 16 renders pages with SSR + ISR and React Server Components. It also carries the customer portal.

  2. A small BFF proxy written in Bun sits between Next and Frappe. It holds the API credentials, whitelists which backend methods can be called at all, caches hot routes in Redis, rate limits per IP, and handles payment gateway callbacks. Frappe is never exposed to the public internet directly.

  3. Meilisearch powers listings and search. Documents are pushed from Frappe on change. Typo-tolerant results in about 8 ms.

  4. Cloudflare in front of everything, respecting our origin cache headers.

Which doctype does what

The rule we kept from day one: custom doctypes only for concepts ERPNext does not have. Anything that touches money or stock stays on standard documents.

Standard ERPNext carries the commerce. Item (with a handful of custom fields and a specifications child table), Item Group as the category tree, Brand, Customer and Address, and the whole sales chain. When a shopper places an order, the cart converts to a normal Sales Order, and from there it is regular ERPNext: Sales Invoice, Delivery Note, Payment Entry. The ops team processes web orders exactly like showroom sales. The portal order timeline is those documents stitched together, and it merges POS invoices too, which is how a showroom purchase shows up next to an online one.

The custom app adds the web-only concepts:

  • Website Route Meta: every public URL is a document. Slug, entity type, enabled flag, meta tags. One resolver endpoint takes a slug and returns the right payload for a PDP, category, brand, landing page or static page. 2,100+ of these, and the SEO AI maintains their meta.

  • eCommerce Attribute (+ options): the filter and spec vocabulary. Which attributes exist, which are filterable or sortable, what type they hold. Search facets are built from this.

  • Item Specification: child rows on Item binding those attributes to products.

  • eCommerce Filtered Catalog: curated landing pages (a base category plus a stored filter), each auto-creating its own route doc.

  • eCommerce Cart (+ items): server-side cart holding EMI selection, coupon, shipping and totals, converted to the Sales Order at checkout.

  • Shipping Method + Rule: weight and territory based delivery pricing.

  • Product Score: precomputed ranking batches (global, per category, per brand, trending) behind bestseller ordering.

  • Plus customer reviews, deal-of-the-day, and two settings singletons.

Payments and delivery

Payments run on local payment gateway. The frontend is gateway-agnostic: it asks the backend to initiate a payment, receives a hosted payment URL, redirects, and polls status afterwards. The gateway calls back through the proxy, Frappe validates the transaction server-side, and a Payment Entry lands against the Sales Order. Gateway credentials never leave the backend. Partial payments are first-class: COD orders in the capital submit without an advance, other orders take an advance online, and any outstanding balance can be paid from the portal later.

Delivery pricing is the two small doctypes above: methods with rules matching weight bands to territories, each with a cost and an ETA. Checkout picks the address, the territory syncs, the matching rule prices the delivery. Fulfilment is a normal Delivery Note, and the last mile runs on our own fleet or shipping provider, planned on the ERP system.

Cache invalidation as one chain

The part I would recommend to anyone building this: treat cache invalidation as one chain. Every publish, price or stock change in ERPNext fires a doc_event webhook to the proxy, which busts all four layers in order: Redis route cache, the Meilisearch document, a targeted Cloudflare URL purge, and a Next.js revalidate. Full-page caching everywhere, and still no stale prices. Cloudflare free tier has no cache tags, so the proxy keeps a URL map per doctype and purges exact URLs.

Numbers, all measured on the box this week

  • 2.92M requests through Cloudflare on our busiest recent day, about 670k of them answered straight from edge cache, and 2M+ on a typical day (storefront, events and services together)

  • 789k requests a day reach the storefront origin, peaking near 2,000 requests/min

  • search responses around 8 ms

  • 25 to 67 ms TTFB through the edge

  • 6,707 active SKUs in ERPNext, about 1,600 published product pages

We started with an Express proxy and later rewrote it Bun-native (Bun.serve, built-in Redis client). Same box, about 4x the throughput in our benchmark. The BFF layer costs almost nothing and buys a lot of safety.

One customer, one portal

Because POS and web share one ERPNext, the customer portal covers offline and online purchases alike. Someone who bought a fridge in a showroom logs in with their phone number and sees that invoice next to their online orders. From the same place they can claim warranty, follow service jobs, manage loyalty points, and pay outstanding balances. No separate CRM, no import jobs, no paper receipt required.

What Frappe was great at, and what we worked around

Great: doctypes as the product model (custom attributes, filtered catalog pages, brands as first-class docs), doc_events driving the invalidation chain, role-based method whitelisting, and one database shared by POS, stock, accounting and web. Worked around: guest-facing API shapes needed a dedicated app rather than the stock REST (you do not want tabItem exposed raw), and file URLs come out relative, so we normalize them at the proxy.

A few AI pieces turned out very practical: a background-removal worker that gives every product photo a clean cutout automatically, an AI SEO writer in the backend that maintains meta titles and descriptions for 2,100+ routes (hash-gated, so it only rewrites when the underlying data changed), an AI-assisted omnichannel support console, and an MCP server that exposes the catalog read-only to AI agents. If you use Claude or any MCP client, you can literally ask it to browse our catalog.

The response has been the best part of this project: genuine excitement from customers reaching our support team, and a clear lift in online sales since the new storefront went live.

The codebase is not public. It is heavily custom built for MK, from the design system to payments. But we plan to isolate MK specific customizations and make a branch that can be released. If there is interest here, that moves up the list.

Happy to answer anything about the setup.

11 Likes

Hi @hiimkhaled

Thanks for posting this. I see many times people ask for sucess stories and best practises. Hope this one is not lost in Forum archives. We need to pin it somewhere in addition to tutorials and best practises

1 Like