Skip to content
Zowork
All case studies
Engineering case study~11 min read

One clinical-notes editor for web, mobile and desktop.

Behavioral-health clinicians can spend as long documenting a session as delivering it. We came on as product owner for a documentation tool built to make that faster and better, and to meet clinicians on whatever they had in hand: a browser, a phone in the session room, or an installed desktop app. Online or offline, it had to be the same editor.

ReactReact NativeRich text (Quill)Offline-firstElectron
progress-note · mobileQuill · WebView
React Native shell offline-ok
expansions
Client reports reduced anxiety since last sessionexpand “pt” → “patient”
nativewebview
RN ↔ WebView · async message-passing
Introduction

One product, three runtimes, zero compromises

The product is a clinical-documentation tool for behavioral-health providers: faster, higher-quality notes, with terminology suggestions tuned to behavioral health, and a clean path from a session to a signed note. It ships in three forms (a web app, a mobile app, and an installable desktop app), all sharing one writing experience.

The client, a US-based IT partner to behavioral-health providers, brought us in as product owner. We were in every stage (requirements, design, development and QA) across all three runtimes, on a deliberately tight timeline.

The brief

What the tool had to do

The goal was simple to state and hard to build: make clinical documentation fast, safe and available anywhere.

  • Speed up documentation with a fluid, low-friction note-taking experience.
  • Behavioral-health-aware suggestions for spelling, grammar and clinical terminology.
  • Capture notes & sessions offline, with no internet required in the room.
  • HIPAA-compliant encryption of data at rest on the device.
  • Meaningful analytics in charts and dashboards, not raw tables.
  • Customizable reports to compile collective data on demand.
How we worked

Product ownership under a tight clock

Delivering a product this broad on a short timeline was as much a process problem as an engineering one.

We took the product-owner seat alongside the US team, joining user-feedback sessions, brainstorming features together, and turning ideas into shippable work. Our UI/UX designers ran a fast loop: mock up an idea, get user feedback, refine, hand to development. That tight loop, on top of a genuinely well-architected codebase, is what let a small team ship a broad product quickly.

Process change that paid off
We moved the team from Scrum to Kanban so work could flow continuously against a moving target, with fewer ceremony boundaries, faster feedback, and a steadier path to release under real time pressure.
Root-cause deep-dive

One rich-text editor, on every surface

The writing experience is the product. It had to feel the same (and be the same code) on the web and in a native mobile app.

On the web we built on Quill, customized with expansions, drop-ins and inline suggestions for a fast clinical writing flow. Then came the hard part: Quill is web-only, and React Native has no equivalent rich-text editor. Rewriting it natively (twice, for Android and iOS) would have fractured the experience and doubled the surface area for bugs.

Our out-of-the-box approach: run the same Quill build inside a WebView on mobile, and bridge it to React Native with asynchronous message-passing. Native code and the editor stay fully decoupled, posting only messages, so one editor, with the same rich features, runs identically on web, Android and iOS.

React Native ⇄ WebView bridgeeditor-bridge.tsx
tsx
1// The rich-text editor is web-only, so we run the SAME Quill build inside a2// WebView and bridge it to React Native with async message-passing. Native and3// editor never touch directly; they only post messages to each other.4 5// ── native side (React Native) ──────────────────────────────────────────────6const editor = useRef<WebView>(null);7 8const send = (type: string, payload?: unknown) =>9  editor.current?.injectJavaScript(10    `window.__editor.receive(${JSON.stringify({ type, payload })}); true;`,11  );12 13<WebView14  ref={editor}15  source={{ uri: 'editor/index.html' }}          // bundled, offline Quill build16  onMessage={(e) => {17    const msg = JSON.parse(e.nativeEvent.data);18    if (msg.type === 'ready')  send('load', note);          // hydrate the editor19    if (msg.type === 'change') persistDelta(msg.payload);   // offline-first save20  }}21/>;22 23// ── editor side (inside the WebView) ────────────────────────────────────────24const RN = (window as any).ReactNativeWebView;25const quill = new Quill('#editor', { modules: { expansions, dropIns } });26 27quill.on('text-change', () =>28  RN.postMessage(JSON.stringify({ type: 'change', payload: quill.getContents() })),29);30(window as any).__editor = {31  receive: ({ type, payload }) => type === 'load' && quill.setContents(payload),32};33RN.postMessage(JSON.stringify({ type: 'ready' }));
Keystroke / scribble
clinician input
Quill (WebView)
expansions · drop-ins
postMessage
async bridge
Persist delta
offline-first
Encrypted SQLite
at rest
Offline + security

Capture anywhere, and encrypt without melting the battery

A session room can have no signal. The app had to capture everything locally, lock it down, and sync when a connection returned.

Clinicians can create sessions and write notes with no connectivity; the device syncs the moment it reconnects. On the device, the app is locked behind biometric authentication and everything is encrypted at rest with crypto-js.

Drawing on what we learned building our earlier offline-first clinical app, we made the encryption frugal: rather than re-encrypt the whole note on every keystroke, we diff state and encrypt only the key-value pairs that actually changed: identical security guarantees, a fraction of the CPU, and a phone that stays cool and responsive.

Selective (delta) encryptionpersist-delta.ts
ts
1// Encrypting the whole note on every keystroke burns CPU on a phone. Instead we2// diff the persisted state and encrypt ONLY the key-value pairs that changed.3import AES from 'crypto-js/aes';4 5function persistDelta(next: NoteState) {6  const prev = store.getState().note;7  const changed = Object.keys(next).filter((k) => next[k] !== prev[k]);8 9  const patch = changed.reduce<Record<string, string>>((acc, k) => {10    acc[k] = AES.encrypt(JSON.stringify(next[k]), sessionKey).toString();11    return acc;                                  // same AES strength, far less work12  }, {});13 14  return db.notes.update(next.id, patch);        // SQLite, encrypted at rest15}
The web app

A data-dense web app that still feels light

Beyond the editor, the web app carries the heavy, data-centric features, built pixel-perfect and well-tested.

Rich note-taking

Customized Quill (expansions, drop-ins, suggestions) for a fast clinical writing flow.

Individual & group notes

Capture notes per client or across a group session, from an appointment or a session.

Scribble → note

Hand-written scribble captured and imported straight into the note-writing process.

Bulk-sign

Sign many notes at once to save clinicians real time at the end of a day.

Dynamic questionnaires

Custom documents with dependent questions and rules; questions imported from the EHR.

Analytics dashboards

After researching the options, we recommended and built dashboards with react-apexcharts.

Customizable reports

Pick parameters and generate reports tailored to what each user needs.

Granular permissions

A permission system that restricts access to specific patients and data.

EHR integration

Web services that integrate cleanly with other EHR solutions.

The desktop app

The same app, installed

For clinicians who prefer a dedicated application, we shipped the web app as a native install.

We packaged the web app as an installable desktop application with Electron. Same features, same code, a desktop-native launch, and no third runtime to maintain.

One codebase, three targets
Web, mobile and desktop draw from one product and one editor. New features land once and show up everywhere: the multiplier that made the timeline possible.
The stack

What it runs on

React

The web app and the shared, data-dense UI.

React Native

The mobile app for Android and iOS.

Quill

The rich-text editor, customized and shared across web and native.

WebView bridge

Async message-passing between React Native and the in-WebView editor.

Electron

The web app, packaged as an installable desktop app.

BlueprintJS

The component foundation for a dense, consistent UI.

ApexCharts

Analytical dashboards and data visualization.

crypto-js + biometrics

Biometric app lock and encryption of data at rest.

SQLite + C# services

Local persistence on device; web services for EHR integration.

Outcome

One editor, everywhere clinicians work

3

Runtimes from one product: web, mobile and desktop

1

Rich-text editor shared across them all via a WebView bridge

0

Bars of signal needed to capture a session

offline-first
  • A fast, fluid note-taking experience, the same on web, Android, iOS and desktop.
  • Sessions and notes captured offline and synced on reconnect, encrypted at rest.
  • Data turned into dashboards and customizable reports, not raw tables.
  • A broad product delivered on a tight timeline through product ownership and a tight design loop.
Conclusion

Share the hard part, not just the easy parts

The temptation with “works everywhere” is to rebuild the experience per platform and watch them drift. We did the opposite: shared the single hardest component, the editor, across every runtime, and let native code do only what it’s good at. The result is one product that feels consistent in a browser, in a clinician’s hand, and on a desktop, online or off.

Need one experience across web, mobile and desktop, without three codebases to keep in sync? That’s the kind of problem we like.

Talk to engineering