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.
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.
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.
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.
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.
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' }));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.
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}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 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.
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.
One editor, everywhere clinicians work
Runtimes from one product: web, mobile and desktop
Rich-text editor shared across them all via a WebView bridge
Bars of signal needed to capture a session
- 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.
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