Topology, not hierarchy: the four shapes
Two axes decide how you deploy neosian: who runs neosian code and where the bytes live. Four shapes fall out, none of them a hierarchy, and the quickstart always begins embedded.
| bytes on local files | bytes in a database | |
|---|---|---|
| your app runs neosian | embed + FileStore: dev, local tools, single-writer agents |
embed + PostgresStore: production, multi-worker |
| a separate process runs neosian | the state process (neosian serve) owning a FileStore root |
the state process over Postgres |
Embed first
Section titled “Embed first”A single app embeds the library and talks to its store directly:
files for dev/local, Postgres for production. PostgresStore is a
driver, not a process: the database is your existing infra, the
SQLAlchemy shape. An embedded app needs no proxy in front of its own
store.
The daemon is reach, not capability
Section titled “The daemon is reach, not capability”The state process (neosian serve) is the
first-class answer when state is shared across processes, apps, or
languages, including one container in a dev compose beside redis
and minio, or when a FileStore root needs more than one writer: one
process owns the files and every client speaks to it. It adds no
capability the library lacks, only reach. docker run is never step
one. And nothing is a one-way door: neosian export DIR writes any
store (a root, Postgres, the daemon by --url) to a directory that
is itself a FileStore root, and neosian import DIR restores it
verbatim into any other, one scope or conversation per request over
the wire, version history and actors carried as they were.
The appliance quickstart
Section titled “The appliance quickstart”One token, one volume, one health check:
docker run -d \ -e NEOSIAN_SERVE_TOKEN=change-me \ -p 6367:6367 -v neosian-state:/data \ ghcr.io/mausa-ai/neosian:<X.Y.Z>curl -fsS http://localhost:6367/healthThe image is published per release tag (amd64 and arm64; latest names
the newest final release); the shipped Dockerfile builds the same image
from a checkout (docker build -t neosian .).
The default command serves a FileStore on the /data volume; set
NEOSIAN_POSTGRES_DSN (and override the command, e.g. --schema neosian) for the Postgres backend. Without the container it is one
command: NEOSIAN_SERVE_TOKEN=… neosian serve, where no flags serve the
home, --root DIR another root. The token is env-only and an unset
token refuses to start; TLS terminates at a reverse proxy.
The home: one place for every project
Section titled “The home: one place for every project”~/.neosian (or $NEOSIAN_HOME) is the store every command and the
playground use when no flag names one, and the root a neosian agent
reaches through home(). Per project is a scope, not a root, and a
machine is registered once: neosian setup --write writes each client’s
own config, which names the home and no mount, and every session
derives its layout where it runs (user:<login> at /user,
user:<login>/proj:<slug> at /project); project_scope() spells the
same for a Conversation. No project carries a file, and a new project
needs nothing. One neosian audit inside a project then lists every
agent’s work in it. (--level project still writes one directory’s own
files: neosian docs agents.)
Many projects and agents on one home is the multi-writer shape, so the
answer is the state process on the home: one process owning the files,
every hook and MCP server registered with --url. As a per-user
service, a docs recipe, never library scope:
<!-- macOS: ~/Library/LaunchAgents/com.neosian.serve.plist --><plist version="1.0"><dict> <key>Label</key><string>com.neosian.serve</string> <key>ProgramArguments</key> <array><string>/path/to/venv/bin/neosian</string><string>serve</string></array> <key>EnvironmentVariables</key> <dict><key>NEOSIAN_SERVE_TOKEN</key><string>change-me</string></dict> <key>RunAtLoad</key><true/><key>KeepAlive</key><true/></dict></plist># Linux: ~/.config/systemd/user/neosian.service[Service]ExecStart=/path/to/venv/bin/neosian serveEnvironmentFile=%h/.config/neosian/serve.env # NEOSIAN_SERVE_TOKEN=…Restart=on-failure[Install]WantedBy=default.targetThen launchctl load / systemctl --user enable --now neosian, and one
run moves every client on the machine behind it:
NEOSIAN_CLIENT_TOKEN=change-me neosian setup --url http://127.0.0.1:6367 --writeThe token is never written into a registration or a hook line: set
NEOSIAN_CLIENT_TOKEN in each client’s own environment (for Claude Code,
the env block of ~/.claude/settings.json; for a shell-launched client,
your shell profile). Muse clears child environments: its MCP entry
forwards the token by an environment reference, and setup --url configures
managed hooks to receive that variable by name. This requires user-level
hooks; an existing foreign managed file requires a manual merge
(neosian docs agents).
Python clients speak the store wire:
from neosian import RemoteStore
store = await RemoteStore.connect("http://localhost:6367", token="change-me")RemoteStore implements both storage ABCs over httpx alone (no
serving stack loaded), so it drops into Conversation and MemoryConfig
exactly where FileStore does. A client in another language
implements the contract neosian docs wire states. Agents speak MCP
over streamable HTTP at /mcp when the server is started with mounts
(--scope or --mount).
Every listing crosses the wire a page at a time. A route answers at most
500 rows and names what to send back: next_cursor for the memory
listings, next_after for a conversation’s turns and projections.
RemoteStore follows the pages for you, so history(scope) still
returns every row. To page yourself, the three shipped stores implement
Pageable: await store.history_page(scope, cursor=None, limit=100)
returns Page(items, next_cursor), where the cursor is opaque and valid
only with the same arguments.
Serving your own store. build_app(store) accepts any store that
implements both storage ABCs. If yours does not also implement
Pageable, its listings answer whole, in one response with
next_cursor: null, and a page or a cursor sent to it is refused by
name. That response holds the entire listing in the process’s memory.
At the KB scale memory is built for this is harmless, but a scope with
millions of version rows can exhaust the process, exactly as the same
call would inside your own application. Implement the four *_page
methods to bound it. neosian serve only opens stores that do.
Who wrote what
Section titled “Who wrote what”The state process asserts identity (DESIGN §20): NEOSIAN_SERVE_TOKEN
may be a table (claude-code:laptop=…,app:kit=…) and every write
through the wire is recorded under the presenting token’s client
(<client>[/<what the client said>]); a bare token is the one client
client:default. Shell entry points reach the process with --url and
NEOSIAN_CLIENT_TOKEN; neosian audit --scope S reads the ledger back
on any substrate, and --url reads it through the process.
The agent door reads as well as writes. A foreign agent’s SessionStart
hook prints the memory index and “where we left off” (the scope’s
recent sessions, log-projected) into its own window, and its MCP
client calls search_history(query) and recall_turn(turn, conversation)
on /mcp (or the stdio server) to find and re-read any recorded turn
verbatim: one client writes, a different client recalls, on the same store (neosian docs agents,
neosian docs mcp).
One writer per root
Section titled “One writer per root”FileStore’s in-process lock serializes mutations inside one process;
across processes, files cannot arbitrate: two writers on one root
can interleave a read-modify-write and lose an edit. neosian
documents the constraint instead of engineering around it: no lock
files, no flock, no leases (they half-promise arbitration at the
price of an NFS/Windows/containers portability matrix and a
stale-lock failure mode, on the substrate whose entire value is that
you can cat it).
A root is owned by one writer at a time: an agent’s shell
(neosian memory), one MCP server process, or one embedding
application. Any number of readers may run beside it; a
concurrent reader is bounded to a stale read, never a corrupted
store. Multi-writer needs route to PostgresStore, which arbitrates
on the version-row primary key, or to the state process, where one
neosian serve owns the files and every client speaks to it over
RemoteStore.
What two projects on one home share. A machine registered once runs
a hook process and an MCP server per session, all on the home, so it is
worth being exact about the rule’s reach. Different scopes are disjoint
files: each scope’s documents, version sidecars and redaction trail live
in its own directory, and a conversation is one directory per id, so
two projects, or two sessions, never write the same file by writing
their own. The shared surface is the scope every project mounts,
/user, and two sessions of the same project. There a race needs two
writers on the same document at the same moment; what it costs is an
edit lost (last writer wins) and, in the sidecar, a version number
given twice, since both read the last number before either appends.
Nothing tears: appends are whole lines and a document is replaced
whole. If that window matters to you, the state process closes it, and
moving a machine there is the one neosian setup --url run above.
Choosing
Section titled “Choosing”- One app, one machine, inspectable state → embed +
FileStore. - One app, many workers or many machines → embed +
PostgresStore. - Many apps or languages sharing one memory, or a FileStore root that
needs more than one writer (the home, once two projects’ hooks or
servers write it) → the state process (
neosian serve). - Changing your mind later →
neosian exportfrom the one,neosian importinto the other; the archive is a FileStore root either way.