Four steps,
about two minutes.
Set a passphrase, name a mesh, put a QR code on screen, and approve the phone that scans it. The onboarding screen budgets 20 seconds for the first step and 40 for the second, and that is roughly honest.
What has to be on the machine.
- Node 22 or newer. The
enginesfield refuses anything older. - Ollama running on
127.0.0.1:11434, with at least one model pulled. Without it the workspace starts, the pages load, and every chat turn fails at the model call. - The skill router binary at
~/.meshvault/tooling/bin/meshvault-skillif you want the skills page to return anything. Its absence is reported as an error rather than an empty result.
Start the daemon:
pnpm dev # next dev -p 3000 -H 127.0.0.1
The -H 127.0.0.1 is not decoration. It is the reason nothing else on your network can reach the workspace, and it is also the first thing that trips people up during pairing. Read step 3 before you reach for your phone.
Set a passphrase.
Open http://127.0.0.1:3000. The first request goes to /login, which asks the daemon whether it has been configured. On a fresh node it has not, so the screen is in setup mode and asks you to choose a passphrase twice.
Minimum eight characters. It is hashed with scrypt against a 16-byte random salt and never written down anywhere, in any form. Your session token is 32 random bytes; only its SHA-256 hash reaches disk. Sessions last 30 days and at most 50 are kept, oldest dropped.
Rotating the passphrase later needs both an authenticated session and the current passphrase. Ten wrong attempts lock logins for 60 seconds, held in memory, which means a restart clears it. scrypt already costs about 100ms per attempt, so that lockout is a speed bump on top of a wall.
This gate authenticates you to your own daemon. It does not defend against another process running as the same user on the same box. That would need an OS keyring, and the source says so rather than implying otherwise.
Name the mesh.
The welcome screen asks for one thing: a name, up to 64 characters. Submitting it posts to /api/mesh, which generates an Ed25519 keypair with node:crypto and writes the result to store/mesh.json.
What you get back is an id, the name, a public key in base64 SPKI, and a fingerprint, which is the first 16 hex characters of the SHA-256 of that public key. The private key stays in the store file and never appears in an API response.
Creating a second mesh on the same node is refused. A new key would orphan every device already enrolled against the old one, so the code fails loudly instead of quietly resetting you.
Put the QR on screen,
then scan it.
Step 2 of the welcome screen mints an invite as soon as it loads. Asking someone to click "show me the code" before showing them the code is a click that buys nothing, so it does not.
The invite is a random id with a ten minute life. The QR encodes exactly one string:
https://<your node>/pair#<invite id>
The encoder is 482 lines in the repository, byte mode, error correction level M, versions 1 to 10, and it emits an SVG path. It exists because every QR package on npm drags in a canvas or PNG stack for output the pairing screen does not want.
Two details worth holding onto. The invite id sits in the URL fragment, and a fragment is never sent to a server, so it stays out of access logs, out of Referer headers and out of anything in between. And the id is a pointer rather than a credential: on its own it enrols nothing. Security covers why.
The workspace binds loopback. A phone on the same wifi cannot reach 127.0.0.1 on your machine, so scanning the code with no forwarding in place gives you a browser error, not a pairing screen.
The phone-side error text says to check you are on the same network, which is necessary and not sufficient. You need a path to the port: an SSH forward, a tunnel, or a run of the daemon bound to an address the phone can actually route to. Binding wider is a deliberate act with its own consequences, so do it knowingly.
Compare six digits.
Then tap approve.
On the phone: the pair page guesses a device label from the user agent, you can edit it, and submitting posts to /api/mesh/enroll. That call raises a pending request and returns six digits. It enrols nothing.
On the machine running the workspace: the same six digits appear beside Approve and Reject. Compare them. If they match, tap Approve.
That tap is the authentication. It posts to /api/mesh/requests, which sits behind the session gate, and only then does a device record get written. The invite is burned in the same step: one QR, one device.
| Limit | Value | Why |
|---|---|---|
| Invite lifetime | 10 minutes | Long enough to walk to a phone |
| Pending request lifetime | 3 minutes | The owner is standing right there |
| Pending requests at once | 8 | Caps a flood of prompts on the owner's screen |
| Invites per approval | 1 | A reusable code on a screen is a standing credential |
Pairing is skippable. Forcing a phone into the first five minutes loses the people who are only looking, and the welcome screen is built to let them past.
Three failures and their causes.
"That invite is expired or not valid"
One message covers both cases on purpose. Telling you which one it was would turn the endpoint into an oracle for guessing invite ids. Mint a fresh code and scan again.
The request expired while you were reaching for the Mac
Three minutes is short by design. The phone shows the expiry, the screen offers the retry, and nothing about the flow is a dead end.
You edited a store file by hand and nothing changed
The daemon caches store documents in memory and flushes writes on a 150ms timer. A file you edit underneath it keeps serving the cached copy, and a pending write can be flushed back over your edit. Restart the service after editing those files by hand.