Run brainy on your own box
In this section
This page takes one Linux machine from nothing to a running brainy: the package installed, a license in place, keys you made yourself, a first brain you can write to and search, a backup, and an agent connected. Every command on it was run on a clean machine, with paths under a temporary directory in place of the ones shown here; the answers quoted are what came back.
Nothing here needs an account with anyone beyond the one that issued your license, and nothing here makes an outside call unless a step says so — the last section lists every one.
What you are building
your agents / your code
| one key each
v
+--------------------+ +----------------------+
| brainy host | | brainy serve | optional,
| takes the writes | <----> | answers reads fast | beside it
+--------------------+ +----------------------+
|
v
/srv/brainy/brains/<one directory per brain>brainy host is the one process that opens your brains for writing. It serves every door over HTTP, every tool over the agent protocol, and a live change feed. brainy serve is the native read server; you can run it beside the host, and a table below says exactly who answers what.
What you need
Machine | Linux on x64 with glibc. This is the one platform this page covers (see the platform table at the end of the install step). |
Node | 22 or newer. |
Disk | Fast local disk. Brains are directories of files that are read in place, so a slow or network disk shows up directly in first-request time. Sizing a brainy server has the measured numbers. |
A license key | One key, from your account at https://soulcraft.com/account. It is checked on the machine, against a public key inside the package; nothing is looked up. |
A language model | Optional. Search and memory work without one. See "Bring your own model". |
1. Install the package and place the license
From: Installation, License artifacts.
Point the @soulcraft scope at the registry once, then install into a directory of its own:
npm config set @soulcraft:registry https://source.soulcraft.com/api/packages/soulcraftlabs/npm/
mkdir -p /opt/brainy && cd /opt/brainy
npm init -y
npm install @soulcraft/brainyBASHThat one install brings the engine, its native file, the native read server and the models the engine needs; it came to 87 packages and about 590 MB. Check what you got:
ls node_modules/@soulcraftBASHbrainy
brainy-model-ms-marco-minilm-l12-v2
brainy-serve-linux-x64-gnuNow the license. Either put it in the environment, or in a file readable only by you; the environment wins if both are set:
export BRAINY_LICENSE="sc_brainy_..."
# or:
mkdir -p ~/.config/soulcraft
printf '%s\n' "sc_brainy_..." > ~/.config/soulcraft/brainy-license
chmod 600 ~/.config/soulcraft/brainy-licenseBASHAsk the engine what it sees. This is the same check a real start makes; it prints the key's source and a fingerprint, never the key, and opens no connection:
npx brainy license statusBASHSource: BRAINY_LICENSE
Fingerprint: <sixteen characters>
Status: valid
Product: brainy
Tier: <your tier>
Licensee: <your address>
Expires: <the date>With no key at all, the same command stops with exit code 1 and tells you where to put one. A start with no key is not quiet either: the host comes up, and the first time it has to open a brain it refuses by name — REFUSED — no Brainy license found — listing the two places it looked. Your data is never held back by a missing key: the store format is open and the MIT reference engine reads it.
Platform table.
Piece | Platform |
|---|---|
The engine's native file (what | Linux x64, glibc — the file in the package is an x86-64 ELF shared object |
The native read server ( | Linux x64, glibc — packaged as |
The model service ( | Linux x64, glibc — an optional package, see step 8 |
On any other platform the packaged pieces above are not available, and the commands that need them refuse by name rather than start half a server.
2. Make the keys file
From: brainy host.
The keys file is how the host knows who is calling. One key per line, three fields: the key, kind:name, and what it reaches. Create the directories and the first line — the service key, whose reach is !. It runs the machine's mechanics (health, backups, creating brains and keys) and never reads or writes a person's rows:
umask 077
mkdir -p /etc/brainy /srv/brainy/brains /srv/brainy/snapshots
printf 'bks_%s service:operator !\n' "$(openssl rand -hex 32)" > /etc/brainy/keysBASHThe file must be readable by you alone: the host refuses to start if it is not mode 0600, and refuses a key shorter than 24 characters. People's keys are not typed in by hand. The host mints them for you, over the wire, in step 3, and appends their lines to this file. The line formats, for reading the file later:
# <key> <kind>:<name> <brains it reaches>
bks_9f3a… service:operator !
bky_2c81… person:you@example.com notes
bky_7d40… service:orders *TEXT* is every brain; ! is the service key; a list of names is exactly those brains.
3. Start the writer, and make your first brain
From: brainy host, brainy serve.
Start the host. Run it by hand first so you can watch it, from the install directory:
npx brainy host \
--brains-dir /srv/brainy/brains \
--listen 127.0.0.1:8400 \
--keys /etc/brainy/keys \
--snapshots-dir /srv/brainy/snapshotsBASHIt prints one line saying what it mounted and how many brains and keys it loaded:
[brainy host] serving 0 brain(s) from /srv/brainy/brains on http://127.0.0.1:8400 — 1 key(s) loaded, 87 routes, MCP at POST /mcpBind it to a loopback or private address, never port 0. It has no TLS of its own: put it behind your own proxy or keep it on a private network.
Save the service key in your shell without printing it, then make the first brain:
SERVICE_KEY=$(awk '$2=="service:operator" {print $1}' /etc/brainy/keys)
curl -sS -X POST http://127.0.0.1:8400/v1/brains \
-H "Authorization: Bearer $SERVICE_KEY" -H 'content-type: application/json' \
-d '{"name":"notes"}'BASH{"brain":"notes","created":true,"root":"/srv/brainy/brains/notes","generation":1}This one call is how a first brain is made, and nothing else is needed. It makes the directory and opens it with the engine, which writes the store's identity, its manifest and its counts ledger — a directory that only looks like a store is exactly what the native read server refuses, so do not make the directory yourself. A name is taken once: asking again answers BrainExistsError (409), and nothing is ever deleted by this door.
Now a key for a person, scoped to that one brain. The answer carries the key once; no door reads it back, so put it somewhere safe now:
curl -sS -X POST http://127.0.0.1:8400/v1/keys \
-H "Authorization: Bearer $SERVICE_KEY" -H 'content-type: application/json' \
-d '{"kind":"person","principal":"you@example.com","brains":["notes"]}'BASHThe answer's key field is the new credential, and its line is already in the keys file — the host honours it on the very next request, no restart.
Write a note as that person, then search for it by meaning:
PERSON_KEY=bky_... # the key from the answer above
curl -sS -X POST http://127.0.0.1:8400/v1/brains/notes/entities \
-H "Authorization: Bearer $PERSON_KEY" -H 'content-type: application/json' \
-d '{"data":"The quarterly plan moves to Thursday.","type":"document","subtype":"note","metadata":{"title":"quarterly plan"}}'BASH{"value":"<the new row's id>","brain":"notes"}curl -sS -X POST http://127.0.0.1:8400/v1/brains/notes/find \
-H "Authorization: Bearer $PERSON_KEY" -H 'content-type: application/json' \
-d '{"query":"when is the quarterly plan"}'BASHThe first result is the note, scored on both its words and its meaning. A write must name a subtype: a brain refuses a write without one (ValidationRefusedError, 400), saying so.
Two refusals worth knowing, both by name. The service key trying to write a row is refused ServiceKeyCannotWriteError — a service is not a person and has no rows of its own to write. A person's key asking for the machine's health is refused ForbiddenError (403): that door wants the service key.
4. Run the writer as a service
From: brainy host.
Put the license in a file the service reads, readable by the service account alone:
printf 'BRAINY_LICENSE=%s\n' "sc_brainy_..." > /etc/brainy/env
chmod 600 /etc/brainy/envBASHThen the unit. Use the full path to your node (command -v node prints it), because a service does not inherit your shell's path — a unit that names the brainy launcher alone failed to start here for exactly that reason:
[Unit]
Description=brainy host - the writer for this machine's brains
After=network-online.target
Wants=network-online.target
[Service]
# User=brainy # the account that owns /srv/brainy, if you made one
# Group=brainy
EnvironmentFile=/etc/brainy/env
ExecStart=/usr/bin/node /opt/brainy/node_modules/@soulcraft/brainy/dist/cli.js host \
--brains-dir /srv/brainy/brains \
--listen 127.0.0.1:8400 \
--keys /etc/brainy/keys \
--snapshots-dir /srv/brainy/snapshots
Restart=on-failure
RestartSec=5
TimeoutStopSec=300
MemoryMax=24G
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetINIInstall and enable it the way you enable any service on your distribution. What was run for this page: the unit was checked with systemd-analyze verify, and the same start line, environment file and memory limit were run as a transient service, which answered on its port and stopped cleanly on a stop request (the stop is handled, in order, as brainy-host-population then brainy-host-server, both reported done, and the process exited with status 0).
Two settings are worth keeping as written. TimeoutStopSec=300 gives a large brain time to flush before the service manager gives up: a stop is registered work that closes every open brain cleanly, and a forced kill leaves a brain that the next start has to treat as abandoned. And MemoryMax is the ceiling the host budgets itself against — it reads its limit from the control group it runs in and prints the budget it derived (for a 30 GiB cap: a 24,453 MiB budget).
5. The native read server, beside it (optional)
From: brainy serve, Sizing.
brainy serve reads the same brains directory, using the same keys file, and holds no writer lock — it ran here beside the host while the host had the brain open. Start it on its own port:
npx brainy serve --brains-dir /srv/brainy/brains --listen 127.0.0.1:8300 --keys /etc/brainy/keysBASHWho takes the writes
Door |
|
|
|---|---|---|
Write a row ( | answers | refuses by name: |
Get a row by id, counts, find by field filter | answers | answers |
Find by meaning ( | answers | refuses by name: |
Make a brain, mint or revoke a key, take a snapshot | answers | not a door it has |
The live change feed | answers, and holds the connection open | ends the stream once it has caught up (it attaches read-only, so nothing can commit in its process) |
Takes the store's writer lock | yes — the only holder | never; it attaches read-only |
The rule to keep: every write goes to the host, and a read that ranks by meaning goes to the host too. The native server is for reads that are filters and lookups, answered without the host's cost. It is optional; the host answers every one of its reads as well.
6. Bring your own model (optional)
From: Language providers.
Search, memory and every read and write on this page need no language model. Only the jobs that write words — classifying a row, summarizing, judging, rewriting — need one, and brainy calls none unless you named one. Which one is yours to choose: a vendor's hosted model by its usual key, a model you run yourself, or anything that speaks the OpenAI-compatible shape.
With none configured, a write that asks for language work still lands — the row is stored and the call answers 200 — and the language job then fails loudly in the host's log, naming the cure:
background job 'language-classify-write' FAILED ... "cure":"no language provider
configured — set a vendor key ..., or run a local model on localhost:11434
(Ollama), :1234 (LM Studio) or :8000 (vLLM); ..."To point it at the model you already run, set the endpoint where the host starts (in /etc/brainy/env for the service). Add the outbound switch to refuse anything that is not on a private address:
BRAINY_LANGUAGE_ENDPOINT=http://127.0.0.1:11434/v1
BRAINY_LANGUAGE_OUTBOUND=offBASHOn the first brain that asks for language work the host narrates what it chose, and calls that endpoint:
language provider: local endpoint at http://127.0.0.1:11434/v1, model <its first listed model>A server on the machine's own address was reached this way (its model list, then its completions endpoint). With OUTBOUND=off, an endpoint outside the private ranges is refused by name — BRAINY_LANGUAGE_OUTBOUND=off refuses '<origin>' — before anything is sent to it; a test pins that no request is made. Two vendor keys with no explicit choice is a refusal, never a silent pick; BRAINY_LANGUAGE_PROVIDER names which.
7. Embeddings stay on the machine
Embedding is built in: the model is in the package and the host computes vectors itself. With nothing set, the host says so once, plainly, when a brain opens — remote embedding: none ... embeddings are computed by the local leg exactly as before. This is the normal state of an install with no GPU service. Nothing is fetched at run time.
8. A separate embedding service on a GPU machine (optional)
From: The brainy CLI.
If you have a GPU machine of your own, the model runner can serve embeddings to the host over your private network. It is an optional package and a license add-on (the services capability). Without the package installed, the command stops and names its cure:
npx brainy embed-service --bind 127.0.0.1:8200BASH[brainy] REFUSED — neither brawn optional package is installed.
[brainy] THE CURE — npm install @soulcraft/brawnBind it to a private address of the GPU machine instead of the loopback one. A host that can resolve the name brainy-embed on port 8200 finds the running service with no configuration at all, checks that it embeds the same way the local engine does, and only then uses it. Setting BRAINY_EMBED_ENDPOINT=local turns discovery off outright. The running service itself is outside the path this page checked: only the refusal above was run.
9. Connect your agents
From: Connect your terminal, MCP.
Start the host with --public-url https://your-door and tell your agent's terminal:
join https://your-doorThe door's own GET /join page answers with the two commands that install its plugin — naming your address, never anyone else's — and its /plugin/marketplace.json serves the plugin archive with its checksum, both without a key. Each person then sets BRAINY_API_KEY to a key you minted for them in step 3 and restarts. Every tool the engine has is served at POST /mcp to the same key, scoped to the brains that key reaches.
10. Back it up
From: Backup and restore.
A snapshot while it runs. The host takes one by name into the folder you placed with --snapshots-dir, cut at one generation while the brain keeps serving. It needs the service key:
curl -sS -X POST http://127.0.0.1:8400/v1/brains/notes/ceremonies/persist \
-H "Authorization: Bearer $SERVICE_KEY" -H 'content-type: application/json' \
-d '{"name":"2026-10-06-nightly"}'
curl -sS http://127.0.0.1:8400/v1/brains/notes/ceremonies/persist \
-H "Authorization: Bearer $SERVICE_KEY"BASHThe second call is the run's status; it ends at "state":"done" with a receipt like {"generation":2,"name":"2026-10-06-nightly"}. Use a new name each night. The host never overwrites or deletes a snapshot.
Restore is a copy and then a normal start, under a name that is not in use (the host lists its brains from the directory on every call):
cp --sparse=always -a /srv/brainy/snapshots/notes/2026-10-06-nightly /srv/brainy/brains/notes-restoredBASHThe restored brain answered the same counts as the original, and the same search.
A plain copy of the whole directory, with the host stopped, is also a backup, and the same command is how you do it:
cp --sparse=always -a /srv/brainy/brains /backup/brains-2026-10-06BASHAlways copy brains sparse-aware. A brain keeps a few preallocated files that look enormous when you ask for their length but occupy almost nothing on disk — the brain made on this page occupies 1.4 MiB and measures 23 MiB by length, and a real store's gap is far wider. A copy tool that does not understand this writes out every empty byte: the copy is slow, can fill the disk, and gives you the wrong picture of what you store. cp --sparse=always -a keeps them empty — here the copy came to the same size as the original (du -sh for what is stored, du --apparent-size -sh for what it looks like). Measure space with du -sh, never with ls -l, and do not use scp for a brain at all, since it cannot preserve holes.
Never open a snapshot folder itself as a brain. And do not copy a brain's directory while the host has it open — use the snapshot door for a running brain: a copy of a live store carries the live writer's lease.
11. What never leaves your box
The license check is a signature verified against a public key inside the package. No network call, at install, at start, or at any later time. A key that expires under a running brain changes nothing for that process.
There is no telemetry. No beacon, no analytics, no phone-home; the absence is pinned by a test.
Your data is files in a directory you own, in an open format.
Language work goes only where you pointed it (step 6); with
BRAINY_LANGUAGE_OUTBOUND=off, only to a private address.Models are in the package; nothing is downloaded at run time.
The outside contacts that exist, each one set by you and none by default: installing the package from the registry (step 1); a language model endpoint or vendor key (step 6); a remote embedding service (step 8); and three host flags that name an outside URL — --auth-server (someone else's sign-in), --reading-door (a service that mints reading links) and --sign-in-mail (your own mail sender for sign-in codes). Leave those flags off and the host speaks only to the callers you gave keys to.
Where each step came from
Step | Page it came from |
|---|---|
1 Install and license | |
2 Keys |
|
3 First brain |
|
4 Service |
|
5 Read server | |
6 Model | |
8 Embedding service | |
9 Connect | |
10 Backup |