ADR-0004 — Vanilla JavaScript, no framework
- Status: Accepted
- Date: 2026-04-15
- Decision-makers: Tom Anderson
Context
The Helm Worker handles ~50 endpoints today, projected to ~150 by full slice completion. The operator UI is a single-page (per shop) operator app with kanban, customer profiles, sales, etc.
Frameworks considered:
- Hono on Workers — neat router, middleware, light. Adds a dependency and a layer of abstraction for routing patterns we can do in 30 lines.
- Remix on Workers — full-stack with SSR. Bundle size triples; SSR adds runtime cost; for a per-shop POS the win is small.
- Next.js on Workers — heavyweight; a non-trivial portion of Next features don't work on the Worker runtime.
- React SPA + thin Worker API — popular pattern; a 200KB React bundle plus the rest is a lot of bytes for the operator's POS to load.
- Vue / Svelte / Solid SPA — same shape as React, smaller bundles. Same conceptual cost as React (a framework to maintain expertise in).
- Vanilla JS + small composition library (HTM, lit-html) — scaffolding without a framework. Closest to "framework-free" while still being ergonomic.
- Plain JS + plain HTML — no library at all; HTML strings rendered server-side, JS modules enhance.
Decision
Worker: plain JavaScript, no router library, no ORM. Path-pattern matching in a switch. Operator UI: server-rendered HTML, enhanced with small JS modules per screen. No SPA framework. No React. No Vue.
Specifically:
- API endpoints:
if (path === '/api/customers' && method === 'GET') return apiCustomersList(...)style routing - Server templates: vanilla template literals or a small render helper; no Handlebars/EJS dependency
- Client JS: small modules per screen (
public/js/customers.js,public/js/service.js); no framework runtime; usesfetch()and DOM API directly
Consequences
Positive:
- Smallest possible Worker bundle (~50KB unminified today)
- Smallest possible HTML/JS payload to the browser; instant load on POS Wi-Fi
- Aligns with progressive enhancement — works without JS
- Aligns with boring tech — fewer dependencies to track
- One developer can hold the whole system in their head
- No framework "rewrite v3" risk
Negative:
- Component reuse requires manual discipline (no JSX, no
<Component />) - Some patterns (form validation, complex stateful interactions) take more code than they would in React
- Onboarding a new developer is "learn vanilla JS again" rather than "drop into the React patterns you already know"
- Hot reload and dev DX is less polished than Vite + React
Mitigations:
- The patterns we repeat are extracted to helpers in
public/js/lib/, not a framework - Forms validate server-side primarily (the Worker is the source of truth); client-side enhancement is small
- Code style + a few worked examples are documented in code style; a new developer starts with those
Notes
If the codebase grows past ~30,000 lines or onboarding becomes a real bottleneck, reconsider. Until then, vanilla wins on cost, simplicity, and shipping speed. We retain the option to introduce a tiny framework (e.g., Lit or Solid) for a single complex screen without rewriting the rest.