NAOMS Devlog

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

NAOMS Is Now an App You Double-Click

The app isn't a prettier window onto NAOMS โ€” it's the on-switch. Here's what 'one process you launch' actually has to do under the hood, and why packaging is the unglamorous work that decides whether anyone but a developer ever runs the thing.

Product Teacher free June 23, 2026ยท8 min readยทdistribution
TL;DR For most of this project's life, running NAOMS meant starting a daemon in a terminal and pointing a browser at the right local address โ€” nothing for a developer, a wall for everyone else. Now it's a native desktop app you double-click. But the interesting part isn't the window. It's that the app has to be the on-switch: it brings up the daemon, keeps it alive, and takes it down cleanly, all as one thing you launch. This is the story of that under-the-hood job, and why getting it right is the difference between a demo and a product. Honestly scoped: macOS first, the other platforms are the road ahead.

There's a kind of barrier that never shows up on a feature list: the barrier of starting the thing. For most of this project's life, using NAOMS meant opening a terminal, running a daemon, and pointing a browser at a local address. If you write software, that's not a barrier at all. If you don't, it's a locked door with the instructions written in a language you don't speak.

That door is open now. NAOMS is a native desktop app: you double-click it like any other application, a window appears, and the whole memory system is running behind it. No terminal. No copied URL. No "is it running yet?"

The easy version of this story is "we put the web UI in a window." That part is real, but it's not the interesting part, and it's not the hard part. The interesting part is a small phrase with a lot hiding inside it: the app is the on-switch. This piece is about what that phrase actually demands โ€” because the demands are where the engineering lives, and where most "just make it an app" attempts quietly fall over.

NAOMS was never one program

To see why packaging is hard, you have to know what's actually being packaged.

From the outside NAOMS looks like one app. Underneath, it has always been two pieces that talk to each other. There's a daemon โ€” a small always-running program that holds your encrypted memory, does the cryptographic work, and answers requests. And there's a UI โ€” the screens you actually look at, which by themselves do nothing but ask the daemon questions and draw the answers. For a long time you started those two pieces by hand: launch the daemon, then open the UI in a browser, then trust that they found each other.

And the daemon is not even a single file. We checked this carefully rather than assuming it, because the whole packaging plan depends on it: the daemon is a script run by a bundled JavaScript runtime, which in turn loads NAOMS's real cryptographic engine โ€” compiled native code โ€” over a foreign-function bridge. So "ship the daemon" really means "ship a runtime, plus a script, plus a compiled crypto library, and make all three find each other on a stranger's computer." A one-click app has to hide that entire arrangement behind a single icon.

That's the unglamorous truth of packaging: the work isn't the window you see, it's the plumbing you're not supposed to notice. We've written before that the best technology is the kind you don't notice. Packaging is exactly that kind of work โ€” invisible when it's right, fatal when it's wrong.

The on-switch problem: a browser tab assumes; an app provides

Here is the design fork that the whole effort turns on.

A browser tab is a guest. When you open the NAOMS UI in a browser, the tab quietly assumes the daemon is already running somewhere โ€” someone, presumably you, started it first. If nobody did, the tab just sits there showing a disconnected screen. The browser has no idea how to bring the daemon to life; that was never its job.

A native app can be a host instead. The whole point of the desktop app is to refuse that assumption. When you double-click it, it starts the daemon. It doesn't wait for the daemon, hope for the daemon, or tell you to go start the daemon. It owns the daemon's whole life: brings it up when the app opens, keeps it running while you work, and shuts it down when you quit. That ownership is what we call shape-B supervision โ€” the app supervises the daemon as part of itself, rather than treating it as a thing the world is supposed to provide.

This is the difference between "a window onto NAOMS" and "NAOMS you can launch." The first is a viewer. The second is a product.

sequenceDiagram
    actor You
    participant App as Desktop app (shell)
    participant Daemon as NAOMS daemon
    participant Memory as Your encrypted memory

    You->>App: double-click
    App->>App: strip first-launch quarantine
    App->>Daemon: start it (one bundled runtime + crypto engine)
    Daemon->>Memory: open the local encrypted store
    App->>App: open the window, point it at the daemon's own address
    Note over App,Daemon: While you work, the app watches the daemon.
If it dies unexpectedly, the app brings it back. You->>App: quit App->>Daemon: take it down โ€” and everything it started โ€” cleanly Note over App,Daemon: No orphaned daemon left holding the port behind your back.

Keeping it alive โ€” and, harder, taking it down

"Start the daemon" sounds like one line of code. The honest version is two ongoing responsibilities, and the second one is where naive attempts leak.

The first is keeping it alive. Long-running programs sometimes die โ€” a bug, a hiccup, a momentary resource crunch. The old browser-and-terminal setup handled this with an operating-system service that would restart the daemon if it fell over. The desktop app takes that job over directly: it watches the daemon, and if the daemon exits when it wasn't supposed to, the app starts a fresh one. You never see it happen. From your side, the system was simply always up.

The second responsibility is taking it down cleanly, and this is the part that sounds trivial and isn't. When you quit the app, the daemon must go too โ€” quitting the window while a hidden daemon keeps running would be a small lie about whether NAOMS is on. But the daemon may have started helpers of its own, and those have helpers, and so on. If you simply kill the daemon first, its children get adopted by the operating system, lose the family link back to the daemon, and keep running as orphans โ€” including one still holding the network port the app needs next time it opens.

So the order is deliberate and load-bearing: take a single snapshot of the whole family while the links are still intact, then kill the children before the parent, from the leaves inward. Get the order wrong and you leak a ghost process that makes the next launch fail for a reason nobody can see. This is the kind of detail that doesn't exist in the demo and absolutely exists in the product โ€” the same lineage as a daemon that never blocks itself: the interesting engineering is in the corners, not the happy path.

The one daemon, byte-for-byte

There's a quiet discipline underneath all of this that we want to name, because it's a value, not just a tactic.

The app does not start its own special version of the daemon. It starts the exact same daemon โ€” same launch arguments, same settings, same environment โ€” that the non-app install starts. The supervisor in the app mirrors the old service-based launcher precisely, so the daemon running inside the app is identical to the daemon running any other way. Only the thing holding the leash changed. That matters because it means the app didn't fork NAOMS into "the desktop version" with its own subtle behaviors and its own subtle bugs. There's one system; the app is just a new front door to it. Two front doors that quietly lead to two different houses is exactly the kind of drift we refuse to ship.

The bug that proves packaging is real work

If you want a single example of why packaging earns its own roadmap item instead of being a Friday-afternoon wrapper, here it is โ€” a real ship-blocker we hit and recorded.

Inside the app, the UI has to load from somewhere. The first design loaded it from a private internal address built into the app. It seemed clean. It also silently broke: the screens rendered, but they could never actually reach the daemon sitting right next to them. Every request to the daemon failed, the UI fell back to a polite "disconnected" state, and onboarding could never finish.

The cause was a browser-security rule with no mercy and no error message a normal person would understand. That private internal address counted as a "secure" origin to the browser engine, while the daemon next door answers over plain local http. Modern browser engines forbid a secure page from talking to a plain one โ€” it's called a mixed-content block, and it's there for good reasons on the open web. Here it meant the app could see its own daemon but was forbidden from speaking to it.

The fix was to stop loading the UI from a clever private address and instead load it straight from the daemon's own local web address โ€” the same address the browser version always used, which the browser engine treats as secure for exactly this loopback case. One origin, no mismatch, nothing blocked. We mention it because it's representative: packaging is a hundred small, unglamorous decisions like this, any one of which can turn a working system into a window that opens onto nothing. None of them are visible in the final double-click. All of them had to be right for the double-click to exist.

What's true today, and what's the road ahead

We hold ourselves to saying plainly what is done and what is not, so here is the honest line.

What is real: the macOS app is built and landed on our main line. It brings up the daemon, supervises it, restarts it if it dies, takes the whole tree down on quit, and loads the real NAOMS UI inside a native window. We verified the mechanism end to end: assembling the signed app and then launching the daemon from the shipped bundle's own bytes โ€” not from a developer's working copy โ€” brought the daemon to a full healthy boot, with the cryptographic engine loaded and the local store open. There is also a single build command that assembles this app shape, sitting alongside the existing daemon-only build, which is left byte-for-byte unchanged.

What is the owner's gate, not ours to claim: the very last hop โ€” a person double-clicking the signed app on their own machine, with their own identity, and walking through real first-time onboarding on screen โ€” is a human verification we don't fake on someone else's behalf. The machinery is proven up to that doorstep. Crossing it is a real person's job, by design.

What is the road ahead: the star this work serves is one-click on macOS, Linux, and Windows alike. That's the direction, not yet a finished claim for all three. macOS is the one we drove first and hardest, because proving the shape once โ€” the supervision, the lifecycle, the clean teardown, the right way to load the UI โ€” is the expensive part. The other platforms are a road now, not a leap, because the shape is settled. We'll say "cross-platform" when cross-platform is true, and not a day before.

The milestone is small to state and large to live: NAOMS is becoming something you open, not something you operate. The window was the easy half. The on-switch was the real one.


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

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