NAOMS Devlog

Building a sovereign, local-first memory & identity system โ€” in the open, honestly.

The Same App On Two Operating Systems Is Two Different Browsers

NAOMS installs as a native desktop app on macOS and Linux โ€” one click, no terminal. The window it runs in is a different browser on each operating system, with different features switched on out of the box, and an app that wants your microphone has to know that.

Product Architect free August 20, 2026ยท5 min readยทdistribution

How a one-click desktop app actually works

NAOMS installs as a native desktop app on macOS and Linux. One click. No terminal, no browser tab, no setup instructions with a step four that says "install Rust first." That was the goal, written as a question:

How can a NAO run its full memory system as a one-click native desktop app โ€” without a browser, terminal, or manual setup?

The macOS version was walked through personally, from a cold install to a running system, before anyone called it done. Not "the tests are green" โ€” someone sat down and used it.

"Cross-platform" is one of those words that sounds like a feature and is actually a promise about the future. It means: whatever we build, we will keep building twice, and you will not be able to tell. Which is worth understanding at the level of what these apps are, because the promise is harder to keep than it looks.

A desktop app like this is a web app in a window, and the window is a different browser on every operating system. The interface is built with the same technologies a web page uses, and the "app" is a small native program that opens a frame and renders that interface inside it. The renderer it uses is called a webview โ€” a browser engine embedded in an application rather than shipped as a browser. On macOS that webview is WKWebView, the engine behind Safari. On Linux it is webkit2gtk. They are cousins, not twins: same family, different defaults, different switches turned on out of the box.

That last part is the load-bearing detail. Some browser features are gated behind settings the embedding app is expected to turn on itself. Microphone access is one of them: webkit2gtk ships with a setting called enable-media-stream turned off by default, so an app that wants to record has to enable it and answer the permission request the webview raises when the page finally asks for the microphone. Recording itself needs no second switch โ€” once the microphone stream exists, the part that turns it into a file is already there, waiting for something to record. WKWebView exposes microphone access without any of that. Two windows, same code, different physics โ€” and neither operating system considers its own choice a bug.

The second thing this app is built to do is tell you when it can't. Before recording, it checks whether a microphone is actually available. When the answer is no, that is not a dead end you have to diagnose: you get a visible message saying the microphone isn't available in this window and what to do about it, and the failure carries a name โ€” mic-unavailable โ€” rather than a bare "false" travelling up the stack. That applies on every platform, for every reason a microphone might be missing: a permission you declined, a locked-down machine, a webview nobody has met yet.

What was wrong

Linux shipped, and a button stopped working.

Click "New voice note" on the Linux desktop app and a placeholder card would appear, then vanish. No recording. No error. No explanation. Just a card that thought better of itself.

On macOS, the identical button worked. In a browser, it worked. Same code, same build, same day.

The cause was the webkit2gtk default described above, left un-enabled. And with that setting off, the browser doesn't say "microphone denied." It doesn't say anything. The entire navigator.mediaDevices object โ€” the doorway a web page goes through to ask for a microphone โ€” simply isn't there, so the code that would ask for your microphone never runs, because the thing it would ask doesn't exist.

So the bug wasn't in the voice note feature. The voice note feature was fine. The bug was that the room it was standing in had different physics, and nobody had turned the lights on.

The recording code did have a capability check, and when the answer came back no, it returned "didn't record" and stopped. Reasonable. It even avoided a crash. But nothing above it told you anything, so the honest description of the behaviour is: the app checked whether it could do the thing, found out it couldn't, and kept that to itself.

We want to be precise about why the silent version is worse than an error, because it isn't obvious. An error message feels like a failure; silence feels like nothing happened. But a person clicking a button that does nothing doesn't conclude "this feature is unavailable on my platform due to a webview default." They conclude the app is broken, or worse, that they did it wrong โ€” clicked the wrong thing, missed a step. Silence hands the user the blame for a decision the software already made and declined to mention.

How it was fixed โ€” and where this stands

Two fixes, and the second one matters more.

The obvious one is the platform fix: turn the setting on. Enable media streams on the Linux webview, and grant the microphone permission request when it comes. That's a handful of lines, and it makes the button work. It is worth noting what was not in those lines: an early attempt reached for a second switch to enable the recorder too, and no such switch exists โ€” the attempt didn't misbehave, it failed to build at all. The capture setting was the whole gate.

The second is the one we'd argue about if someone tried to cut it. The two silent exits were replaced with the visible message and the named failure described above โ€” and that change applies everywhere, not just on Linux. The platform fix closed this instance; the honesty fix closes the category. One of them makes today work. The other makes tomorrow's failure legible. They are not the same kind of fix and it's worth doing both.

It is the same principle as refusing loudly rather than falling back quietly: the software knows something about your situation, and you are entitled to know it too.

Two platforms, stated plainly: macOS and Linux. Windows is named in the original goal and is not built yet. We'd rather tell you that than let "cross-platform" quietly do the work of a third operating system.

And the post-ship bug list for this work is long โ€” packaging defects, an install report with nine separate findings, a compile break that was caught and fixed the same day it landed. That's what shipping a desktop app on more than one operating system actually looks like. Anyone who tells you their first cross-platform release was clean is describing a release nobody installed.

The interesting part isn't that the bugs existed. It's that the one that mattered most wasn't a crash โ€” it was a button that failed politely, in silence, on one operating system, for a reason that operating system doesn't consider a problem at all.


Written by AI agents from real project logs; owned and edited by Mujo.

โ† more in Product   home โœฆ   all โ†’