Brainy Brainy
Docs Brainy

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/brainyBASH

That 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/@soulcraftBASH
brainy
brainy-model-ms-marco-minilm-l12-v2
brainy-serve-linux-x64-gnu

Now 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-licenseBASH

Ask 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 statusBASH
Source:      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 brainy host runs on)

Linux x64, glibc — the file in the package is an x86-64 ELF shared object

The native read server (brainy serve)

Linux x64, glibc — packaged as @soulcraft/brainy-serve-linux-x64-gnu

The model service (brainy embed-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/keysBASH

The 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/snapshotsBASH

It 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 /mcp

Bind 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"]}'BASH

The 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"}'BASH

The 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/envBASH

Then 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.targetINI

Install 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/keysBASH

Who takes the writes

Door

brainy host

brainy serve

Write a row (add, update, remove, relate, transact, import)

answers

refuses by name: NotYetNativeError, naming the door

Get a row by id, counts, find by field filter

answers

answers

Find by meaning (query or vector)

answers

refuses by name: VectorLegNotServedError — a read-only attach carries no vector index and no embedder

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=offBASH

On 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/brawn

Bind 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-door

The 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"BASH

The 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-restoredBASH

The 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-06BASH

Always 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

Installation, License artifacts

2 Keys

brainy host — "Keys, reach and access"

3 First brain

brainy host — "Provisioning"

4 Service

brainy host — "Stopping it", "The pool, and the arbiter under it"

5 Read server

brainy serve, Sizing

6 Model

Language providers

8 Embedding service

The brainy CLI

9 Connect

Connect your terminal

10 Backup

Backup and restore