Case study

Rebuilding the Kiyoh review management platform

From a monolithic AngularJS portal inside a FreeMarker shell to a modern Nx-based, multi-app React architecture, plus a parallel Next.js widget rebuild that replaces dozens of legacy templates.

Decoupled deployments, strict typing, shared libraries, and a platform structure that scales with teams and tenants.

Kiyoh served thousands of businesses with review collection, moderation, analytics, and publication. But the frontend powering it was a single aging portal that had accumulated nearly a decade of coupling and tooling debt.

The legacy client-portal ran on AngularJS 1.5 + TypeScript 2.0, bundled via Gulp 3 + Webpack 1, rendered inside a Java-served FreeMarker shell. Four distinct personas were implemented as conditional runtime route registration inside one monolithic SPA.

The stack was end-of-life, onboarding was slow, and UI changes were tightly coupled to backend release cycles. We needed a rebuild that decoupled deployments, enforced structure, and preserved full feature parity.

The vision

Rebuild the platform with independent frontend deployment, persona separation, shared libraries, and modern tooling while maintaining full feature parity.

Decouple the frontend

Independent builds and deploys without tying UI delivery to Java backend release cycles.

Split personas into apps

Tenant admins, business owners, group managers, and super-admins become first-class applications rather than conditional branches.

Share foundations, not bundles

A shared library layer provides UI, hooks, models, and data access while keeping applications independently buildable.

The solution: kv-frontend

An Nx monorepo containing four React applications and shared libraries. Each persona has its own entrypoint and deploy pipeline, while sharing a common component and data foundation.

kv-frontend/
├── apps/
│   ├── kv-frontend          → Admin Portal (Tenant Administrators)
│   ├── kv-location          → Business Portal (Location Owners)
│   ├── kv-location-dashboard → Group Portal (Multi-location Managers)
│   ├── kv-super-admin       → Platform Admin (Super Administrators)
│   ├── routes               → Unified route tree (shared across apps)
│   └── proxy                → Development proxy server
├── libs/
│   ├── shared               → UI components, hooks, utilities, theme
│   ├── data-access          → Redux store, API layer, models
│   └── grid                 → Data table components (TanStack Table)

Technology transformation

DimensionLegacy (client-portal)New (kv-frontend)
FrameworkAngularJS 1.5React 19
LanguageTypeScript 2.0TypeScript 5.9 (strict)
Build systemGulp 3 + Webpack 1Nx 22 + Vite 7
Routingangular-ui-routerReact Router v6
State management$rootScope + servicesRedux Toolkit 2
StylingLESS (per-tenant)SCSS with shared tokens
FormsAngularJS directivesReact Hook Form + Yup
Tables/gridsCustom directivesTanStack Table + shared grid lib
i18nangular-translatei18next (HTTP backend)
CI/CDCoupled to backend deployDocker + Kubernetes + Nx Cloud

Key accomplishments

Four portals, full feature parity

  • Admin, Business, Group, and Platform Admin portals rebuilt as lazy-loaded React routes with feature-based code splitting.
  • URL compatibility preserved via path-based routing on a single domain (`/`, `/sme`, `/location-group`, `/admin`).

Shared library ecosystem

  • `@kv-frontend/shared` for UI components, hooks, and theme tokens.
  • `@kv-frontend/data-access` for Redux slices, API client, typed models, and auth utilities.
  • `@kv-frontend/grid` for a reusable TanStack Table-based grid layer.

Migration by the numbers

MetricLegacyNew
Applications1 monolithic SPA4 dedicated apps
Shared librariesAd-hoc3 typed Nx libraries
Build & devMinutes, full reloadsSeconds, Vite HMR + Nx cache
Deploy couplingTied to Java backendIndependent Docker/K8s
Code splittingNoneRoute-level + chunked features

Parallel effort: widget modernization

Alongside the portal rebuild, embeddable review widgets were migrated from a Java FreeMarker renderer to a standalone Next.js application, enabling iframe-level cutover and instant rollback.

From templates to registry-driven React

  • Replaced FreeMarker widget sprawl with a registry mapping widget keys to families and React implementations.
  • Kept the data contract and replaced the renderer. No backend changes required.
  • Introduced a contain-scale shell using `ResizeObserver` for pixel-perfect iframe embedding.

Outcome for teams

  • Faster iteration: new variants become a registry entry + assets.
  • Decoupled deploy: widgets ship independently from the portal.
  • Safer migration: per-widget cutover with rollback via DNS/proxy.

Impact

The platform moved from end-of-life tooling and a monolithic SPA to a structure that supports long-term maintainability: strict typing, enforced boundaries, independent deploys, and shared libraries that scale across teams and tenants.

Independentfrontend releases

UI ships without requiring backend deployments.

Structuredmulti-app architecture

Personas split into first-class apps with shared foundations.

StrictTypeScript + linting

Quality gates and boundaries reduce regressions.

Fasterdeveloper feedback

Vite HMR + Nx cache improves iteration and confidence.

Want the implementation details?

Contact meBack to work