Post

Mind Your App: Part 2, The Journey from Prototype to App

The journey from a server-backed prototype to a fully client-side browser app that reads and writes iThoughtsX .itmz files - covering the POC, the old-iPad constraint, round-trip saving, mobile editing, search, and what comes next with Tauri.

Mind Your App: Part 2, The Journey from Prototype to App

While you have been waiting

In my last post, I said I was building a replacement for iThoughtsX. The plan was plain JavaScript on the frontend, a Python FastAPI backend, and “write once, run anywhere.” First goal: a minimal outline viewer with iThoughtsX keyboard commands.

What I didn’t mention was that I ended up building a working server-backed version - and it’s what serves ithoughtsx.com today.

It’s implemented as a full client server architecture. A Python backend parses .itmz files, holds the map tree in memory, and manages every edit over a WebSocket. The browser is essentially a thin view layer. It works. Drag-and-drop, keyboard navigation, inline editing, lazy loading of folded branches over the wire.

I went this way because I wanted multi-user collaborative editing - many people opening the same map, editing together, the backend syncing everyone’s changes in real time.

In retrospect, that idea doesn’t appeal to everyone, and it probably wasn’t the right first move. Better to build something simpler, with no backend dependency, that anyone can run on almost any device. Something that gives people who depend on iThoughtsX access back to their orphaned maps.

If you’re one of those people, you know the feeling. You’ve got years of thinking tied up in .itmz files - knowledge bases, project plans, meeting notes, journals - and the only way at them is to keep an old machine around with the original app installed. I’ve got maps dating back to 2011, some with over 70,000 nodes. There is nothing more frustrating than not being able to reach your own data unless you fire up an ageing machine.

So I narrowed the scope. Get as many people as possible able to open, view, edit, and save their existing maps, on whatever device they have, without installing anything. Collaboration can wait.

The way I see it now, the server-backed prototype has become the “website” I serve “the app” from. Once the app is on your device, it doesn’t need the backend. It becomes yours. It doesn’t depend on a cloud that might disappear.

Which left one question: can I do it purely in the browser? Time for a proof of concept.

Proving it in the browser

Could the browser do everything the server used to do? Parse the file, render the map, handle editing, and - critically - save the changes back?

I built a POC to find out. My main worry before starting: could it actually save? Turns out it can, though not very well unless you’re on Chrome desktop. But at least you can download on most platforms.

The .itmz file is a ZIP archive containing XML (plus embedded images, styles, and attachments). The POC decodes the ZIP in-browser using JSZip and parses the XML with the browser’s built-in DOMParser. I keep the original ZIP in memory so everything inside it - images, styles, metadata - is preserved on save, even if the app doesn’t do anything with it yet.

How fast? My largest test map - 734 KB, 10,362 topics - parses and renders in 138 milliseconds. Faster than the old server-backed version ever managed. Can it scale? Without lazy loading and proper memory management in the browser, probably not. But we can deal with that later.

File access is where the browser landscape gets messy. Modern Chrome and Edge on desktop support the File System Access API, so you can open a file, edit it, and save it back to the same file silently - no download dialog, no “copy (1)” annoyances. Safari and Firefox don’t support it, and probably never will. Mobile browsers don’t either. So the app uses progressive enhancement: where the File System Access API exists, use it for silent save-back. Everywhere else, fall back to a standard file picker for opening and a download for saving.

Install the app as a PWA and it gets better. On desktop Chrome/Edge and on iOS, the File Handling API registers .itmz files with the OS - you tap a file in your file manager and it opens directly in the app, no manual picker. On Android, the Web Share Target API does the same via the share sheet: long-press a .itmz file, tap Share, and iThoughtsX is right there. This was tricky to get working, so yell out if you don’t see it. It took three separate fixes: an absolute URL in the manifest (relative paths silently fail on Android), accepting application/octet-stream (Android file managers don’t know what .itmz is, so they report it as generic binary), and a Cache-Control: no-cache header on the manifest (otherwise Android caches a stale copy and never sees the share target entry). Plus, for me at least - you shouldn’t need it - a phone reboot, because Android caches share-sheet registrations at the OS level and it took me a few goes to figure that out.

The POC proved you can open a file, modify it, reconstruct the ZIP with your changes, and write it back. The file stays a valid .itmz that the original iThoughtsX app (or this one) can reopen. Everything in the original file is preserved.

That convinced me. The browser could own the whole thing. No server required for the core function. The server-backed approach was officially superseded. I’m not super happy with file handling, but at least we get something on every device. Read on.

The old iPad constraint

Here’s where the story takes a turn that shaped the architecture and the technology choices.

I have a couple of original iPad 2s (hand-me-ups from the kids). They run iOS 9. And I feel strongly that we shouldn’t be throwing perfectly good hardware into landfill just because the OS vendor stopped shipping updates. Old tablets should stay alive and useful. E-waste is a real problem, and keeping a 2011-era iPad working in 2026 makes me feel a little better about it.

So I decided the app had to run on that iPad.

This constraint drove real architecture decisions, not just polyfills. Here’s why.

Modern JavaScript uses ES modules - the import and export syntax. ES modules don’t exist on iOS 9. Safari didn’t add support until 10.1, and these old iPads are stuck on 9. So I couldn’t use them. Instead I used RequireJS - an older module system that loads scripts via AMD define() calls. RequireJS is already proven on this hardware, and it maps cleanly to the module patterns in the existing code. In 2026, using a module system from 2011 looks eccentric. But it means the app runs on hardware from 2011, and that’s the point.

Then came testing. How do you run automated tests against an iPad 2 running iOS 9? You can’t just point Playwright at it. The WebKit inspector protocol that tools use to talk to Safari is silently broken on iOS 12 and below. The debugger connection just doesn’t work. Dead end. I love a challenge.

The workaround was a testing harness built from scratch. The idea: inject a small JavaScript file into the page that opens a WebSocket back to my Mac. My Mac sends JavaScript as text, the stub eval()s it on the iPad, and sends the result back. It’s a remote control for a browser that can’t be debugged any other way.

Building that harness was a saga in itself. There was the arrayBuffer() polyfill - the method I used to read .itmz files doesn’t exist on iOS 12 Safari, so I fell back to the older FileReader API. There was the auto-reconnect logic, because iOS Safari aggressively kills background WebSocket connections. There was the safeSerialize() function, because you can’t send a DOM element or a function over a WebSocket without serialising it first, and naive JSON chokes on circular references.

And there was the teardown timeout. After all the tests passed, the suite hung for exactly 121 seconds before exiting. I traced it to the WebSocket handler on the server side - it blocks forever waiting for the iPad’s next message, and the default shutdown timeout is 120 seconds. One line of config - shutdown_timeout=1.0 - brought teardown from 121 seconds down to 2. Comfortable enough for me.

The result: the app runs on a real iPad 2 from 2011. If you’ve got an old tablet in a drawer, this is one more reason to charge it up instead of binning it. I’m going to sit mine in the kitchen with my recipe mind map open. :smile:

From POC to app

The POC proved the concept. Now it had to become a real application. I broke this into stages, building up from foundation to full editing.

BTW: Thank-you to everyone who tried the POC early and sent feedback. Bug reports from real devices are invaluable!

First, the shell: the application chrome. A menu bar (File, View, Help), a status bar along the bottom showing live topic count, depth, fold count, and a modified timestamp, and an About dialog. Then the PWA layer - a manifest so the app can be installed to your home screen or desktop, a service worker for offline support, and a self-update system that checks for new versions and prompts you to reload.

That last bit - the self-updating PWA - turned out to be harder than it looks. Service worker updates are a dark art. A new version of the app shouldn’t take over silently while you’re mid-edit, because a page reload would lose unsaved work. So the app holds the new version in a “waiting” state and shows a toast: “A new version is available - Reload.” You tap it only when you’re ready.

In practice, three separate bugs showed up on real Android devices during testing. The Reload button did nothing in one scenario. The About dialog showed a stale version because the service worker was serving a cached HTML page. And the update toast reappeared after you tapped Reload, forcing you to dismiss it twice. Each was a different failure mode of the same update handshake between the page and the service worker. All fixed, but it’s a reminder that “it works on my machine” is not the same as “it works.”

Then there was a fourth, sneakier bug that I didn’t catch until mobile users kept reporting updates weren’t arriving. The service worker’s install handler was precaching assets - fetching each file and storing it for offline use. But it was fetching them with the browser’s default cache mode, which means “check the HTTP cache first.” On a version bump, the new service worker would fetch app.js from the browser’s HTTP cache - getting the old version - and store that in the new service worker cache. So even after the update, the app was serving stale JavaScript. Desktop worked fine (different cache behaviour, or a force-refresh cleared it). Mobile, especially iOS standalone, was aggressively caching. The fix was to fetch every precache asset with cache: 'no-store' - bypassing the HTTP cache entirely and always going to the network. This bug had been silently affecting every code update on mobile since launch, not just the one that exposed it.

Editing was the big one. The old server-backed app had nearly 2,000 lines of interaction code - selection, keyboard navigation, inline text editing, drag-and-drop, fold/unfold. All of it sent WebSocket messages to the server for every action. In the standalone app, there’s no server. Every action has to mutate the in-memory tree directly and update the DOM on the spot.

I also had to align the DOM structure. The POC used one set of CSS class names; the old app used another. Rather than rewrite all the interaction code to match the POC’s structure, I ported the old app’s structure into the new renderer.

Where it stands today

The beta is now live at ithoughtsx.com/beta.

Here’s what works:

  • Open .itmz files via file picker, drag-and-drop, or - on an installed PWA - OS file association. On Android, iThoughtsX shows up in the share sheet when you share a .itmz file. On iOS and desktop Chrome/Edge, tapping a .itmz file opens it directly in the app. Full round-trip save on Chrome and Edge desktop (open, edit, save back to the same file). Save As or download on any browser, including Safari, Firefox, and mobile.
  • View and navigate the full tree with fold/unfold, keyboard navigation, expand/collapse to any level. Status bar and toolbar visibility are toggleable from the View menu, with your preference remembered between sessions.
  • Edit - inline text editing, create children and siblings, delete nodes, drag-and-drop to reparent, reorder siblings with Cmd+Arrows, promote or demote with Cmd+Left/Right. All synced back to the file on save.
  • Search - full-text search across the entire map or scoped to a subtree. Live incremental results as you type, with a match counter and next/previous navigation. Folded branches auto-expand to reveal matches, then re-fold when you move on. Open with Ctrl+F.
  • Mobile editing - on touch devices, a bottom toolbar with buttons for navigation, add child, add sibling, edit text, fold/unfold, and search. No keyboard required. The app is fully usable on phones and tablets.
  • Cross-platform - Chrome, Firefox, Safari, Edge. Installable as a PWA with offline support and self-updates. Tested on a real iPad 2 running iOS 9, plus modern Android and iOS.

Where to next

The priorities come down to one question: what gets the most people able to use their maps?

Native apps via Tauri. This is where the “write once, run anywhere” ambition from my first post finally gets an answer. Tauri wraps a web app in a native shell - real file open and save dialogs on desktop and Android, without the bloated footprint of something like Electron.

On mobile, this means no more “download a copy” every time you save. You get real save-back-to-file, the way a native app should work. And because Tauri compiles to Windows, Mac, Linux, and Android from the same codebase, it’s a short path from “works in a browser” to “installable native app.” Less maintenance for me.

Longer term, if there’s enough interest, I’ll look at registering the app with the various app marketplaces. But that’s a later conversation.

Reflecting back on why I’m doing this again

I use mind maps for almost everything. Meeting notes, project planning, knowledge bases, presentations. Pretty much every time I give a talk, someone comes up afterwards and asks “what app is that? It’s really cool.” (I refuse to use PowerPoint.) iThoughtsX was that app - fast, keyboard-driven, uncluttered. It became part of how I think.

When Craig Scott announced that toketaWare was ceasing trading, that tool started dying. Existing installs keep working, for now, until some future OS update breaks them. But you can’t buy it anymore. You can’t even download it on a new device. And the promised Android app will never arrive.

As I said in the first post, I tried the alternatives. I really tried to like Obsidian, but it just feels clunky compared to the keyboard commands I got used to in iThoughtsX. The others - SimpleMind, MindNode, XMind - are fine, but they’re not the same. Migrating to a different tool means learning a different way of working, and I already have years of muscle memory telling me how it should work.

So here we are. The goal hasn’t changed since the first post: keep my second brain alive, and help anyone else in the same boat. The scope has shifted a bit, and the tools have matured to the point where this now seems very doable.

Go to ithoughtsx.com/beta, open an .itmz file, and have a go. If you’ve got complex maps - nested folds, links, notes, summaries, colours and so on - open them and let me know if anything renders incorrectly or gets lost on save. If you’re on mobile, install it as a PWA and try the touch toolbar. Tell me what still feels missing or awkward, or which of the many still-unimplemented features matter most to you. If you want to be notified when the native app lands, drop me a message and I’ll add you to the list.

The original iThoughtsX is by Toketaware. This is an independent, browser-based implementation that reads and writes the same .itmz file format. It’s a solo project, built to keep old maps - and old tablets - alive.


This is part two of the Mind Your App series. Read part one.

This post is licensed under CC BY 4.0 by the author.