NAOMS Devlog

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

Build Your First NAOMS Package

One command scaffolds an app with its own window inside NAOMS.

Technology Teacher free September 27, 2026ยท3 min readยทsdk
TL;DR One command, `naoms package init`, scaffolds a NAOMS app with a manifest, a screen and tests, and you can write one record and see it in its window.

One command creates an app with its own window inside NAOMS, the local-first platform that runs on your own machine:

The hello-world window with one row reading "Hello, NAOMS", what you will have at the end of this post

That app is a package: a folder with a manifest (a file naming the package and the data it declares) and a screen (a small JavaScript file the NAOMS app shows as a window). A node is one running NAOMS on your machine, and its background program is the daemon. Why build one: you get a window and a place to keep records your node signs (explained below), and in our run the daemon picked the package up with no registration step.

Scaffold it

From a NAOMS checkout with the naoms tool installed (see docs/build/builders/getting-started/install.md):

cd src/packages
naoms package init --name hello-world --type feature --category productivity --ui

--type feature makes a feature package, --category productivity picks the app drawer (the app's list of installed packages) it appears under, and --ui also fills in the manifest's UI settings (the scaffold writes a screen file either way). The command creates manifest.ts, ui/hello-world-tab.js, starter folders for logic (enrichers, reducers, routes and so on) each with one sample file, and a tests/ folder with a unit, an integration and an end-to-end test. The two files that matter are manifest.ts and ui/hello-world-tab.js.

naoms manifest validate prints Manifest valid, and naoms test run src/packages/hello-world/tests/uc-hello-world.test.ts runs the unit test (2 passed); we did not run the integration or end-to-end tests.

The manifest

The node reads manifest.ts before running any of your code:

export const MANIFEST: NaomsFeatureManifest = {
  id: "hello-world",
  chainTypes: [{
    chainType: "hello-world",
    access: "read-write",
    eventTypes: [
      { type: "hello-world.created", nodeKind: "hello_world" },
      // ... hello-world.updated
    ],
  }],
  graphTypes: [{ nodeType: "hello_world", access: "read-write" }],
  defaultInstallContext: "personal",
  uiModule: "hello-world/hello-world-tab.js",
  // ...
};
  • chainTypes declares the events the package appends to the chain, NAOMS's signed, append-only log.
  • graphTypes declares the kinds of records the package reads or writes in the graph, NAOMS's store of linked records built from that log. A package you scaffold on your own node is treated as yours, so these lists are not enforced against it: ctx.graphQuery({type:"contact"}), a type not listed here, returned real data. The lists are meant to bound packages from other people; that path is planned, not built.
  • defaultInstallContext says where the package's authority lives by default. personal means the package belongs to you alone; the other settings are for shared spaces.
  • uiModule points at the screen: <packageId>/<filename>, without the ui/ folder. The node serves it at /features/<packageId>/<filename>.

The screen

The screen is a plain script with no imports. The node hands it a context object, ctx, with everything it may use. Reading data is one call:

async activate() {
  const resp = await this.ctx.graphQuery({
    type: "hello_world",
    orderBy: ["updatedAt:desc"],
    limit: 50,
  });
  // ... builds one row per record
}

The query type is hello_world, with an underscore, matching graphTypes, not the package id.

Boot it and see it

Run these in a throwaway terminal; they replace HOME for the whole shell.

export NAOMS_DIR=/tmp/hello-world-test/dir
export NAOMS_DATA_DIR=/tmp/hello-world-test/data
export NAOMS_HOME=/tmp/hello-world-test/home
export HOME=/tmp/hello-world-test/home
naoms daemon start --port 34177 --no-watchdog
naoms onboard founder --daemon-url ws://127.0.0.1:34177 --yes --no-local-llm --allow-no-resume --json

naoms onboard founder creates the first account on a fresh node. It prints a 24-word recovery phrase, the master key for that account. Treat it as a real key even on a scratch node: do not reuse or share it. Open http://127.0.0.1:34177/app.html, choose "Use mnemonic instead", paste the phrase and set a password for this browser. "Hello World" appears in the app drawer under productivity, with an empty list.

Now write one record through the package's own declared event type. This is the path we used: open the browser's developer console, use its context dropdown (it reads "top" by default) to pick the Hello World window's frame, so that ctx is in scope:

await ctx.chainAppend("hello-world", "hello-world.created", {
  id: "hw-1",
  title: "Hello, NAOMS",
});

That appends a signed event to the chain. Reopen or reload the window and its next query reads it back as the row in the picture.

You now have a signed record written and read back through your own package. The next step is docs/build/packages/manifest-reference.md, which lists every manifest field.


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

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