Lang on Rock
Guides

Running a server

Serve a data directory over a socket or TCP, with folders to import and watch only if you want them.

The daemon serves every tenant in its data directory. Native tenants need nothing else: a write through the API commits to the database, and a tenant that does not exist yet is created by its first write. A folder of Markdown is optional, for teams that keep editing files in a repository and want the database to follow.

Start it

langonrock serve --data ~/okf/data --socket /tmp/okf.sock
langonrock serving /home/me/okf/data on /tmp/okf.sock (0 tokens, 0 writable tenants)

Without --socket, the daemon listens on <data>/langonrock.sock.

Talk to it

langonrock query "okf+unix:///tmp/okf.sock?tenant=acme" manifest
langonrock query "okf+unix:///tmp/okf.sock?tenant=acme" search orders

Every client that connects shares this one process, its open tenants and their warm search indexes, so no agent invocation pays a cold start.

What the banner counts

The last line reports how many tokens loaded and how many folders the daemon is watching:

langonrock serving /home/me/okf/data on /tmp/okf.sock (0 tokens, 1 writable tenant)

"Writable tenant" there counts the entries in sources.json, not the tenants that accept writes. Under the CLI daemon every native tenant accepts a write from a token granted write access, or from any caller on a socket with no tokens, so 0 writable tenants does not mean the store is read-only.

Zero tokens on a socket is a valid configuration. Zero tokens on TCP never starts.

Importing and watching a folder

Register a folder per tenant in sources.json, next to the data:

mkdir -p ~/okf/sources/acme/sales ~/okf/data
echo '{ "acme": "/home/me/okf/sources/acme" }' > ~/okf/data/sources.json

Write the path in full. Nothing expands ~ inside a JSON string, and every immediate subdirectory of the folder is a bundle.

At startup the daemon imports each registered folder and keeps watching it, one watcher per entry:

snapshot cd9078b9af0b (new), 2 bundles [ops sales], 3 concepts, 1236 bytes on disk
watching /home/me/okf/sources/acme for tenant acme
langonrock serving /home/me/okf/data on /tmp/okf.sock (0 tokens, 1 writable tenant)

Those three lines say it imported, it is watching, and it is listening. You do not need a separate import first, and you do not need langonrock watch in another terminal. Running both would put two watchers on one tenant.

The folder and the database can both change, and neither silently wins. An unchanged file in the folder never overwrites an edit made through the API; a file changed in the folder is imported; a document changed differently in both fails the whole import, and the watcher reports the conflict instead of picking a side. Nothing is ever written back to the folder. Import, watch and export covers the rules.

ENOENT: no such file or directory, watch '...'

A path in sources.json does not exist. The folder has to be there before the server starts, and every immediate subdirectory of it is a bundle, so sources/acme/sales/orders.md works where sources/acme/orders.md gives you an empty tenant.

Tuning the watcher

FlagDefaultWhat it controls
--debounce200 msHow long to coalesce filesystem events before importing
--rescan30 sFull rescan interval, the backstop for missed events

Watch events are a hint, never the whole story. FSEvents coalesces, ReadDirectoryChangesW overflows its buffer under bursts, and inotify has a watch limit. The rescan writes nothing when the tree is unchanged, but it is not free: at 5,000 concepts it takes 84 ms of work to conclude that nothing happened, against 167 ms for an import that changes one concept.

--debounce only coalesces changes made to the folder. Writes through the API commit directly and never wait on it.

Watching one extra tenant

langonrock serve --data ~/okf/data --tenant beta --watch ~/okf/sources/beta

--watch keeps one more tenant importing from a folder without recording it in sources.json, so it lasts exactly as long as that command line. sources.json is the better answer in almost every case, since it survives a restart.

Over TCP

langonrock serve --data ~/okf/data --host 127.0.0.1 --port 7777

The default host is 127.0.0.1 and the default port is 7777. A TCP bind always requires tokens, including on loopback, and any host past loopback requires TLS. Both rules are enforced at startup rather than per request. See Tokens and TLS.

Reading the output

Progress goes to stderr so stdout stays clean for piping, and Bun paints anything written with console.error red. Those lines are status, not failures.

From the library

serve() is the same server without the CLI around it, and it starts read-only:

import { serve } from 'langonrock';

serve({ root: '/var/data', unix: '/tmp/okf.sock', writable: true });

The CLI daemon sets writable: true itself. A library caller must set it, or pass a sourceDir callback that authorizes each tenant, before any write route accepts a change; without either, a write is refused with a 409 that names sources.json. TLS is passed as PEM text rather than file paths, so the module never touches the filesystem to start.

Memory is the practical ceiling

The daemon keeps the 16 most recently used tenants open, each with its BM25 index in memory: 1.1 MB of heap at 500 concepts, 8.1 MB at 5,000 and 32 MB at 20,000. In the engine benchmarks, a fresh process that opens 20,000 concepts, builds the index and searches peaks at 0.36 GB resident.

Each folder import rebuilds the index right away, so the first search after a save to the folder does not pay for it. A commit through the API does not warm it: the first search after one builds the index, 19 ms at 500 concepts and 578 ms at 20,000.

On this page