Rebuilding the Obermind frontend: Vite, React 19 and one less server-rendered stack
Alexander Talei
Founder at Obermind

Obermind Platform 2026.11.3 is the first release on a rebuilt frontend.
The user app, the operator console and the server pages around them all changed in one week. This post covers what we replaced, why, and what we measured before and after.
Where we started
The user app was built on a toolchain its maintainers had deprecated. It still worked, but every upgrade had to go through it.
Three problems kept coming back:
- Slow builds. A production build of the user app took 98 seconds.
- A heavy first load. A signed-in user downloaded 1,682 KB (gzipped) before the home page could render.
- A large dependency tree. The user app installed 1,829 packages, most of them build tooling we never shipped.
The operator console had its own older pipeline. It produced one 12.6 MB script, and a branch build spent 10 to 20 minutes on image optimisation, vendor copies and a second compile.
Around both apps sat a layer of server-rendered pages: sign-in, registration steps, account settings and old admin screens. They duplicated screens the apps already had.
What we changed
The work came in four steps, each released and measured separately.
- User app to Vite. We replaced the build toolchain and kept the application code.
- Lazy pages and a smaller shell. Each page became its own chunk, loaded only when the user can open it.
- React 19 and a new router. We moved the mobile UI layer two major versions forward, then moved to React 19.
- Retire the server-rendered pages. Sign-in, onboarding and settings now live in the user app. The operator console moved to a small dedicated build.
Builds: minutes to seconds
The user app's production bundle now builds in 6 to 10 seconds.
The full build, including a complete type check of the codebase, takes 48 seconds. That is half the old 98 seconds, which included a similar check.
The operator console changed more:
- Before: a 10 to 20 minute branch build, producing one 12.6 MB script
- After: about 25 seconds on a cold cache and about 8 seconds warm
- Entry script: 669 KB (194 KB gzipped), down from 12.6 MB
The rest of the console is split by screen. Opening Users or Framework downloads that screen and nothing else.
The pipeline itself did not need rewriting. The console's build step kept its name, so the existing deployment job calls it unchanged.
What users download
On the user app, the first load of the home page fell from 1,682 KB to 889 KB gzipped. That is 47% less before the first screen renders.
Three changes produced most of that:
- Pages load on demand. A page's code is requested only after the user is allowed to open it.
- Vendor code is grouped by who uses it. The app shell no longer pulls in the data grid, the terminal's charting engine or the 3D globe.
- One shared chart stack. We consolidated our charting code, which cut the home page's chart bundle by about 28%.
Tab buttons also prefetch their page when the pointer reaches them. By the time the click lands, the code is usually already there.
Faster at runtime
In a production build, signed in, against live prices:
- Home → Trade: the new page is visible in about 0.10 seconds
- Trade → Home: about 0.43 seconds
- Idle on home with streaming prices: 120 fps, no long tasks
- Memory across six screen changes: flat, between 199 and 221 MB
One finding was worth sharing. Rendering route changes as low-priority transitions looked like a free win.
In a trading app it was not. Every price update is urgent work, and each one restarted the transition. The old page stayed on screen for up to two seconds. We turned that behaviour off and recorded why in the code.
A smaller dependency tree
Fewer packages means faster installs, smaller CI caches and less to keep current.
- User app: 1,829 → 846 installed packages (−54%)
- Operator console: 1,734 → 467 installed packages (−73%)
- Operator console, direct dependencies: 152 → 27
We also self-host the app fonts now. Neither app makes a third-party font request.
Retiring the server-rendered pages
We deleted 125 server-rendered views and the code behind them.
Sign-in, registration, KYC questionnaires, source of funds and account settings are now pages in the user app. They share its components, its theming and its white-label branding.
What remains on the server is small:
- Two views: the shell that mounts the operator console
- Redirects for links already sent in emails and push notifications
- JSON and file endpoints, and provider callbacks
Links already sent in emails, push notifications and QR codes keep working. They redirect permanently to the new pages.
The same test cases run against the app's router and the server's redirect, so the two cannot drift apart.
Deployments and caching
Every file the build emits is content-hashed. The server sends those files with a one-year immutable cache header.
The app's entry page is never cached. A new deployment reaches users on their next load, while unchanged code is served from the browser cache.
The operator console's publish step now starts from a clean output folder. Stale files from earlier builds no longer ship, and a clean output is about 54 MB.
When a deployment replaces a page chunk while a user has the app open, the app notices. It reloads once to pick up the new version, with a guard so a broken deployment cannot cause a reload loop.
Compliance checks in one place
Every signed-in page in the user app goes through one gate: signed in, KYC loaded, KYC complete, verification done, area allowed. Only then is the page's code requested.
The route registry has no way to add a page without that gate. A new screen gets the checks by default.
Trade-offs
Not everything got smaller.
- Sign-in pages are slightly heavier, 438 KB against 374 KB. The UI component layer is one chunk, which the app needs straight after sign-in anyway.
- The browser floor moved. The app now needs Safari and iOS 16 or later, in the browser and in the native shell.
- The dev server serves source files individually. A signed-in page loads hundreds of modules in development. Production is unaffected.
Audited after the refactor
Once the refactor was complete, we ran a full audit of the user app, the operator console and the server.
No exploitable vulnerabilities were found at any level.
Quality gates
The migration was done in small, reversible steps. Each step was checked in a production build before the next began.
- 102 unit tests pass, including routing, the compliance gate and every legacy-link case
- Lint: zero errors across the user app
- 374 component props were updated by a syntax-tree codemod, not by hand
- Device checks on the iOS and Android shells cover back navigation, deep links, safe areas and resume
What this means for licensees
Nothing changes in how you deploy. You take 2026.11.3 as a set, like any Obermind Platform release.
Your users get a faster first load and quicker navigation. Your operators get a lighter console. Your engineers get builds measured in seconds.
Get started
Read the 2026.11.3 changelog on your deployment, then talk to us about scheduling the upgrade.
