mb mybillbook private beta

2026-05-11 · 6 min read · Engineering

Why mb is HTMX over Go, not React over a JSON API

Every SaaS template since ~2017 has used the same architecture: React (or Vue / Svelte / whatever) talking to a JSON API. The frontend ships a 1+ MB JS bundle; the backend serves JSON; the glue layer maintains two type systems.

mb doesn't. It's HTMX over Go's html/template, with zero JS framework on the browser. Total client-side JavaScript on the dashboard page: under 25 KB (HTMX itself plus a few inline handlers for forms). Here's why.

Who actually uses mb

The target user is an Indian SMB owner or their accountant. The device is a 2–3 year-old Android phone or a Windows 10 laptop in a small office. The network is typically 4G with intermittent dead zones; in tier-3 cities and small towns it's slower than that.

The frontend has to render fast on cold start, work on a sub-100 Lighthouse score is a hard fail. JS bundles measured in megabytes are not on the table.

The HTMX bet

HTMX is a small JS library (~15 KB gzipped) that adds attributes to HTML elements: hx-get, hx-post, hx-target, hx-swap. The server returns HTML fragments; HTMX swaps them into the page. There is no JavaScript framework, no virtual DOM, no client-side router.

The pattern works because the user's needs on a tax-software screen are simple:

None of this requires client-side state management. The server knows what to render. HTMX is the wire that connects them.

Concrete: the ITC eligibility hint

On the new-purchase form, when the user types an HSN code, mb suggests a default ITC eligibility (eligible / ineligible / blocked) based on §17(5) of the CGST Act. Motor vehicle HSN codes get auto-flagged as Blocked, food/beverage codes likewise, and so on.

In a React SaaS, this would be: client-side JS module imports a list of blocked HSN codes, runs the regex match in the browser, updates a piece of state, the component re-renders. Bundle size cost: a few KB for the rule table, the regex engine is free.

In HTMX, this is:

<input
  name="line_hsn_sac"
  hx-get="/purchases/_itc_suggest"
  hx-include="closest tr"
  hx-target="closest tr find .itc-suggest-hint"
  hx-trigger="change, keyup changed delay:500ms"
  ...
/>
<span class="itc-suggest-hint"></span>

The endpoint /purchases/_itc_suggest is a 30-line Go handler that returns an empty body or a tiny <small> snippet. The browser receives 0 to ~50 bytes per keystroke, with a 500 ms debounce. The "rule table" lives only on the server, so updating the §17(5) interpretations is a one-line code change with no bundle re-deploy.

What we give up

Honesty time. HTMX is the wrong choice for:

None of these matter for GST invoicing. If they did, we'd reach for a SPA. Probably Svelte or solid-js — but not React, which is a separate post.

What we get

Numbers from staging

On staging.billmybill.com today, the dashboard page loads in ~120 ms on a Mumbai-Bangalore Indian-broadband connection, transfers about 47 KB total (HTML + CSS + HTMX + fonts subset), and renders without any client-side hydration step. The same flow on a typical Indian Jio 4G with 2-bar reception is around 350 ms.

A comparable invoicing SaaS built in React tends to ship 1–3 MB of JS and render in 2–4 seconds on the same network. The user sees a spinner during that gap.

When we'd reconsider

If we built a mobile app, we'd ship native or a real React Native front. If we added real-time multi-user collaboration on the same invoice draft, we'd reach for WebSockets and a frontend framework to manage the state machine. Neither is on the roadmap.

The point

The choice of architecture has consequences for who can use your product. SPA-by-default has gradually but firmly excluded users on slow networks and old devices — which in India is most users. HTMX is the right tool for the workflow we're building, for the people we're building it for.


Want the longer version with numbers per page? Drop us a line.