# What Virtues is Source: https://virtues.com/docs Virtues is a server that holds the data of your life — messages, calendar, health, location, transactions, recordings, files — on a machine you own, and the software that turns that pile into something readable: a record of your days, articles about the people and places in them, and an AI that reasons from your actual life rather than a generic profile. You can run Virtues on your own Linux machine today. Purpose-built hardware is optional and coming soon. ## Where data lives vs. where it is processed **Your data lives on your hardware and reasoning happens in the cloud.** Virtues stores your database, records, embeddings, and reranking models locally on your disk. However, local hardware rarely has the capacity to run frontier reasoning models. When Virtues performs external reasoning requests (such as composing an account of your day, transcribing audio, or answering user queries), the necessary context passes through our gateway to curated model providers via explicit zero-data-retention pipelines. We meter what a request costs for billing, but we never keep what was in it. **You can bring your own API keys.** If you prefer, Virtues can route requests through any OpenAI-compatible endpoint or personal provider account instead. In that scenario, request payloads bypass our gateway entirely, and data retention is governed by your direct terms with that provider. **Virtues has no administrative access to your server.** Devices authenticate against an authorization list maintained entirely by your local instance. We aren't on that list. We maintain no backdoors and open no inbound ports. [Reaching your server](/docs/operate/reach) details the exact network paths and specifies what our relay can and cannot see. ## Where to start **If we built your hardware,** inference and install are already done. Your next step is to pair your phone or laptop: [Reaching your server](/docs/operate/reach). **If you're setting up your own Linux machine,** three steps: 1. Check [what to run it on](/docs/setup/requirements): minimum specs and a supported OS. 2. Set up [inference](/docs/inference) the embedding and reranking models that search your record. Do it before you install, because the installer asks for their URLs. 3. Run the [installer](/docs/setup/install): one command, about ten minutes. Then pair your phone or laptop: [Reaching your server](/docs/operate/reach). Once you're up and running, four pages cover day-to-day operation: [Upgrading](/docs/operate/upgrading), [Backup & restore](/docs/operate/backup-and-restore) (read it before you need it, since the server cannot decrypt its own archives), [When something breaks](/docs/operate/recovery), and [The CLI](/docs/operate/cli). The [Glossary](/docs/understand/glossary) defines the words this system uses, which is worth ten minutes if the vocabulary feels invented. It partly is. ## What's here, and what isn't These docs are written alongside the software and describe what actually ships. Pages marked _soon_ in the sidebar are planned. Nothing published here is aspirational — if a page says the server does something, it does. Every page here is also plain markdown: append `.md` to any docs URL to get the source, and [`/llms.txt`](/llms.txt) indexes them all for anything reading on your behalf. The manual is versioned with the software it describes — this site publishes from the released branch, so what you read is what a server actually runs. The engineering record behind all of this — design decisions, audits, measured findings — lives in the [repository](https://github.com/virtues-os/virtues) rather than here. It is written for the people and agents building Virtues, and it reads that way. The [Library](/library) holds the essays — what we think this is for. That's the other register entirely. --- # What to run it on Source: https://virtues.com/docs/setup/requirements The short answer: a Linux machine with 8 GB of RAM, a real SSD, and an ordinary internet connection. **No GPU is required.** The rest of this page is what each of those is for, so you can tell which corner you're cutting when you cut one. ## The machine - **Linux**, `x86_64` or `aarch64`, with **systemd** and root. - **Debian or Ubuntu**, or **Fedora**. The installer drives `apt` on the first two — adding the PGDG repository where the distribution's own packages ship an older PostgreSQL than the one Virtues needs — and `dnf` on Fedora, whose PostgreSQL is recent enough as shipped. - **Not macOS or Windows.** Installing needs root, a native PostgreSQL cluster, and systemd units of its own. The Mac and iPhone apps are *clients* that talk to a server; neither is one. - A **virtual machine is fine**. A Docker or LXC container generally is not: the installer writes systemd units and expects to own the machine's services. An old laptop, a NUC-class mini PC, a rack server you already have, or a capable single-board computer all qualify. What separates a pleasant server from a miserable one is almost entirely memory and disk, in that order. ## Memory Eight gigabytes is the floor, and here is where it goes. PostgreSQL and the Virtues server itself want a couple of gigabytes between them. The two model servers — the embedder and the reranker, if you run them on this machine — sit at roughly a gigabyte of resident memory *each* with the flags Virtues starts them under: one request slot, no prompt cache, a 2048-token context. Started with a model server's own defaults they would take about two and a half gigabytes each, which is the difference between fitting and swapping on an 8 GB board. If you run the models on a different machine — the recommended arrangement, and the subject of [Setting up inference](/docs/inference) — then 8 GB here is roomy rather than tight. ## Disk Installing wants **about 4 GB free on `/`**: PostgreSQL, the binaries, the local models if they live here, and working room. After that the record grows for as long as you feed it, in two places under `/var/lib/virtues` — the PostgreSQL cluster and the file store that holds raw attachments, recordings, and documents. **The medium matters more than the capacity.** Vector search is random-read heavy and the write-ahead log is fsync heavy, so storage is the single loudest determinant of whether searching your own life feels instant. The installer classifies and measures the disk under the data directory *before* it provisions anything, and tells you which tier you're on: | What it finds | What it says | |---|---| | NVMe, or a SATA/other SSD | Great — this is the intended case | | eMMC | Workable to around a hundred thousand items; large index builds are slow and the flash wears | | microSD | Searches will feel slow and the card will wear out under database load | | USB-attached | Works, but some USB bridges don't honor cache flushes, which risks corruption on power loss | | Spinning disk | Expect multi-second searches | | NFS or SMB | Don't. PostgreSQL on a network filesystem is a known corruption risk | It also times `fsync` and says so when the number is physically impossible for the medium — a disk that acknowledges flushes without performing them will lose data on a power cut, and that is worth knowing before your record is on it rather than after. None of these warnings block the install; they inform it. `virtues doctor` re-reports storage later. To put the data somewhere other than `/var/lib/virtues`, set `DATA_DIR` when you run the installer, and point it at a **local** disk. ## The accelerator question This is where people overbuy, so it's worth being precise about which models run where. **The model that writes does not run on your server.** Composing an account of your day, answering a question, transcribing a recording — that work goes to a model provider, through our gateway by default or through an endpoint you choose. So there is no VRAM budget to plan for it. **What runs locally is retrieval:** one embedding model, which turns your record into vectors, and one reranker, which re-scores search results for precision. Both are small, and they want *opposite* hardware: - **Embedding is CPU-friendly.** The model Virtues ships has fp32 activations, so fp16 GPU paths fall back to fp32 and come out slower than the CPU. The unit Virtues installs runs the embedder with GPU offload explicitly disabled, on purpose. - **Reranking is the half that gains from a GPU.** The installed unit offloads it fully, and on hardware with a usable GPU backend it is markedly faster there than on CPU. **An NPU is only useful if its vendor ships a server.** llama.cpp supports essentially no NPUs today — its Hexagon backend is a newer-generation, Android-only affair — so neural accelerators reach Virtues by speaking the two HTTP contracts from behind a vendor's own runtime, which is exactly what bring-your-own inference is for. **The number to care about is embedding latency.** The installer measures the p50 of your endpoint and grades it: under 100 ms and searches feel instant, up to 400 ms and they feel a little slow, past that every search in the product waits on it. That measurement, not a spec sheet, is the answer to "is this machine fast enough". ## Network Outbound only. Virtues opens **no inbound port** and needs no forwarding rule; a paired device reaches the server by key, over paths described in [Reaching your server](/docs/operate/reach). During the install it needs to reach `github.com` for the release and `apt.postgresql.org` for PostgreSQL, and it probes both before touching anything. Locally it binds four ports: `8000` for the server, `5432` for PostgreSQL, and `18181`/`18182` for the embedding and rerank endpoints when those run on the same machine. The installer warns if something already holds them. ## What we actually test `x86_64` Debian and Ubuntu servers, and our own `aarch64` board. Everything else is yours to measure — which is a real answer rather than a dodge, because the tools to measure it ship with the installer: the storage verdict and the embedding-latency verdict during setup, then `virtues doctor` on the running server. Next: [Setting up inference](/docs/inference), which is the one piece worth standing up *before* you install, and then [Installing](/docs/setup/install). --- # Setting up inference Source: https://virtues.com/docs/inference Search over your own life needs two small models running near your data: an **embedder**, which turns everything in your record into vectors, and a **reranker**, which re-scores the candidates a search turns up. Neither is the model that writes — that one is remote, and this page has nothing to do with it. Skip this page if you're on hardware we build - both are set up for you. On your own machine they are **yours to run**, and standing them up is worth doing *before* you install, because the installer asks for their URLs and refuses to guess. ## What Virtues consumes Two HTTP contracts: | Endpoint | Required | Used for | |---|---|---| | `POST /v1/embeddings` | Yes | Indexing your record, and embedding every query | | `POST /v1/rerank` | No | Precision on the results of a search | Without an embedder there is no semantic search and no indexing at all. Without a reranker search still works, ranked by vector similarity and lexical fusion alone, at slightly lower precision — a real option, not a degraded mode to be ashamed of. Both endpoints must be on **your own machine, your LAN, or your VPN**. The installer refuses a public address: loopback, RFC1918, link-local, CGNAT (Tailscale and friends), and IPv6 unique-local addresses pass; anything globally routable does not. Cloud embedding APIs are deliberately unsupported, which is why nothing here asks you for an API key. `VIRTUES_ALLOW_REMOTE_INFERENCE=1` overrides the check, is logged when it does, and means what it says: inference traffic may leave your network. ## Why this is yours to run We provision inference on exactly one board — our own, where we know the accelerator, the driver stack, and what happens after a power cut. We do not install GPU or NPU inference software on hardware we cannot test, because doing so produces more broken servers than it saves keystrokes. So the generic path is that you own the endpoint and we validate it at the door. The installer offers three answers: - **Our hardware** — detected from the device tree. Inference is built in. - **Bring your own endpoint** *(recommended for everything else)* — you run the servers; the installer probes them, records what it found, and pins it. - **Quick trial** — a bundled CPU-only model server with our two models and no configuration. Honestly slow, explicitly not a deployment. It exists so you can watch the product move in five minutes; stand up real endpoints before you load real data. Headless installs skip the picker with `VIRTUES_INFERENCE=manual` (plus `VIRTUES_EMBED_URL`, optionally `VIRTUES_RERANK_URL`) or `VIRTUES_INFERENCE=bundled`. ## Standing up the endpoints [llama.cpp](https://github.com/ggml-org/llama.cpp)'s `llama-server` speaks both contracts and is what we run in production, so the recipes below are the ones our own units use rather than a plausible guess. One server per model — a single `llama-server` process hosts one model, and the two want different flags. **The embedder**, on port 18181: ```bash llama-server --embedding --pooling mean \ -m embeddinggemma-300m-qat-Q8_0.gguf \ --host 127.0.0.1 --port 18181 \ -c 2048 -b 2048 -ub 2048 -np 1 --cache-ram 0 -ngl 0 ``` **The reranker**, on port 18182: ```bash llama-server --rerank --pooling rank \ -m gte-reranker-modernbert-base-Q8_0.gguf \ --host 127.0.0.1 --port 18182 \ -c 2048 -b 2048 -ub 2048 -np 1 --cache-ram 0 -ngl 99 ``` What the flags are doing, since these are the ones that matter: - **`--pooling mean` for the embedder, `--pooling rank` for the reranker.** Not interchangeable. The reranker is a cross-encoder; `rank` is what makes `/v1/rerank` exist at all. - **`-ngl 0` on the embedder, `-ngl 99` on the reranker.** The two workloads want opposite hardware — see the reasoning in [What to run it on](/docs/setup/requirements#the-accelerator-question). If you have no GPU, `-ngl 99` is harmless; if you do, the reranker needs whatever group membership your distribution puts on the GPU device nodes, or the backend quietly falls back to CPU. - **`-c/-b/-ub 2048`.** Both models handle longer, but Virtues indexes in windows of about 128 tokens and caps rerank documents near 256, so a larger context buys nothing and costs half a gigabyte of buffers. - **`-np 1` and `--cache-ram 0`.** One request slot instead of the automatic four, and no prompt cache — every input here is unique, so the cache is pure reservation. Together with the context size these cut each server from roughly 2.5 GB resident to about 1 GB. If you want the servers to survive a reboot, run each as a systemd unit. The two units Virtues writes on the bundled path — `virtues-embed.service` and `virtues-rerank.service`, both in `/etc/systemd/system/` — are a fair template: loopback-only, unprivileged, `ProtectSystem=strict`, `Restart=on-failure` with a start limit so a permanently broken server ends up visibly `failed` rather than restarting forever. Other servers work too, as long as they speak the contract below — [Ollama](https://ollama.com), vLLM, or a vendor's NPU runtime. Read [the contract](#the-contract-in-full) before you commit to one; there is exactly one required route per endpoint, and it is the obvious one. You do not need to build llama.cpp to try it: every Virtues release ships a portable **CPU-only** `llama-server` and puts it at `/usr/local/bin/llama-server`. That's enough for the embedder. For a GPU reranker, use a build made for your hardware. ## Choosing models Anything that emits sentence embeddings will work. These are the ones we run or would reach for, with the two properties that actually matter for configuration — the width of the vector, and whether the model wants a prefix on its inputs: | Embedding model | Dims | Prompt prefixes | |---|---|---| | **EmbeddingGemma-300M** *(what we ship)* | 768, truncatable to 256 | `task: search result \| query: ` / `title: none \| text: ` | | gte-small | 384 | none | | bge-small-en-v1.5 | 384 | query only: `Represent this sentence for searching relevant passages: ` | | e5-small-v2 | 384 | `query: ` / `passage: ` | | nomic-embed-text-v1.5 | 768, truncatable | `search_query: ` / `search_document: ` | | Reranker | Note | |---|---| | **gte-reranker-modernbert-base** *(what we ship)* | Cross-encoder, served by llama.cpp directly | | bge-reranker-v2-m3 | Multilingual, larger | | jina-reranker-v2 | Multilingual | GGUF builds of all of these are on Hugging Face; search the model name plus `gguf`. Quantized to Q8_0, the pair we ship is about half a gigabyte together, and these are the exact builds we run: ```bash curl -fLO https://huggingface.co/ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/resolve/main/embeddinggemma-300m-qat-Q8_0.gguf curl -fLO https://huggingface.co/keisuke-miyako/gte-reranker-modernbert-base-gguf-q8_0/resolve/main/gte-reranker-modernbert-base-Q8_0.gguf ``` Four things to know before you pick: **Prefixes are a property of the model, not of Virtues.** An asymmetric model embedded without its prefixes loses recall quality; a symmetric model given prefixes gains noise in every vector. The installer resolves them from the model name for the five families above, and you can always set `VIRTUES_EMBED_QUERY_PROMPT` / `VIRTUES_EMBED_DOC_PROMPT` yourself — your model's `config_sentence_transformers.json` is where its own answer lives. Unknown model, no prefix, is the safe default and what you get. **Width is probed, not assumed.** Whatever your model emits becomes the width of the vector column, up to **4000 dimensions**, which is the ceiling pgvector's index supports. Above that you must truncate. **Truncation is only safe for models trained for it.** Setting `VIRTUES_EMBED_DIMS` slices vectors to a narrower width — a third of the storage and a faster index for very little quality on a Matryoshka-trained model like EmbeddingGemma or nomic. On a model that was *not* trained that way it destroys the vector. It is opt-in per model, never a default, and asking for a width wider than the model emits is an error rather than something we pad. **One model at a time.** Two models' vectors are geometrically incomparable, so a single embedding model owns the index. Changing it is a deliberate re-embed, described below. ## Pointing Virtues at them With the servers running, install: ```bash curl -sSL https://virtues.com/sh | sudo sh ``` The first thing it asks — before it touches a package, a service, or a disk — is how you want inference. Choose bring-your-own and give it the two URLs (`http://localhost:18181` and `http://localhost:18182` for the recipes above; the rerank prompt takes an empty answer). It then probes what you gave it and prints what it found: the vector width, a latency verdict, and whether the reranker answered. Everything it learned lands in the server's environment file at `/var/lib/virtues/virtues.env`, which is where you go to change any of it later: | Variable | Meaning | |---|---| | `VIRTUES_EMBED_URL` | Base URL of the embedding server | | `VIRTUES_RERANK_URL` | Base URL of the rerank server, if you have one | | `VIRTUES_EMBED_MODEL` | The `model` name sent in each request. llama.cpp ignores it; Ollama routes by it and 404s on a name it hasn't pulled | | `VIRTUES_EMBED_DIMS` | Stored vector width — set only to truncate a Matryoshka model | | `VIRTUES_EMBED_QUERY_PROMPT` / `_DOC_PROMPT` | The model's prefixes, quoted so trailing spaces survive | | `VIRTUES_EMBED_FINGERPRINT` | Pinned identity of the model behind the URL | ## What gets pinned, and why At setup the installer embeds two fixed probe strings and hashes the vectors that come back. That hash is the model's fingerprint, and the server recomputes it at every boot. This exists because of a failure that is otherwise silent. Swap the model behind an unchanged URL and every vector already in the index belongs to a different geometry: search doesn't error, it just quietly returns the wrong things forever. So on a mismatch the server **refuses to serve search** rather than answering from an index it can no longer trust. Recovering from that is one command: ```bash virtues configure-inference ``` It re-probes the endpoint, reports what changed, and — on your confirmation — clears the derived index, re-pins the fingerprint and dimensions, resizes the vector column, and lets indexing rebuild. **Your source data is never touched.** Embeddings are a cache; treat them as one. `virtues reindex` rebuilds the same index without the endpoint having changed. Two caveats worth knowing. A quantization change reads as a different model, so re-quantizing the same weights currently costs you a re-embed you didn't strictly need. And moving a working endpoint to a new address is *not* a model change: the fingerprint matches and nothing is rebuilt. ## The contract in full If you're bringing a server we haven't named, this is exactly what it must do. **`GET /health` is optional.** It isn't part of the OpenAI shape, and plenty of good servers don't have it — Ollama answers 404 there while serving embeddings perfectly. Virtues asks for it when the embedder and reranker start, purely as a shortcut: a 2xx means "ready" without paying for an inference, and `llama-server` answers it once the model is loaded. Any other answer is treated as *unknown*, not as failure, and the endpoint is then verified by asking it to embed — which the runtime does immediately afterwards regardless. What is fatal is nothing accepting a connection at all. `virtues doctor` follows the same ladder, so a server without the route reads as `✓ serving (no /health)` rather than being condemned. **`POST /v1/embeddings`** takes `{"input": [...], "model": "..."}` — always an array, `model` always present — and must return: ```json { "data": [ { "index": 0, "embedding": [0.01, -0.02] } ] } ``` Rows are matched by `index`, so order on the wire doesn't matter, but a top-level array with no `data` object will not parse. **`POST /v1/rerank`** takes `{"query": "...", "documents": [...], "top_n": N}` and must return: ```json { "results": [ { "index": 0, "relevance_score": 0.87 } ] } ``` The score field may be spelled `relevance_score` (llama.cpp's and Jina's spelling, above) or `score` (Cohere-style); both are accepted, at setup and at search time alike. The number is read as an ordering only — Virtues sorts by it and then normalizes — so any strictly increasing scale works, bounded or not. **`GET /v1/models`** is optional. When a server offers it, Virtues records what it says it is serving and stamps that on every indexed row. ## Verifying Against the servers directly: ```bash curl -s localhost:18181/health curl -s localhost:18181/v1/embeddings \ -H 'content-type: application/json' \ -d '{"model":"default","input":["hello"]}' | head -c 200 curl -s localhost:18182/v1/rerank \ -H 'content-type: application/json' \ -d '{"query":"cat","documents":["a cat","a bicycle"],"top_n":2}' ``` Then against the server, which is the answer that counts: ```bash virtues doctor ``` Its Inference section names the accelerator it resolved, the models on disk, and — separately, because the two questions are not the same — whether anything is actually *serving* at each URL. A server whose model server has been crash-looping for a week still has both model files exactly where they were put; only the live rows can tell you that search is broken. Every finding comes with the command that diagnoses it. ## Changing your mind later Moving from the bundled trial to your own endpoints, or from one model to another, is the same short path: start the new servers, edit the URLs in `/var/lib/virtues/virtues.env`, run `virtues configure-inference`, and let it re-embed. Nothing about the decision is baked in at install time except which services the installer provisioned, and on the bundled path those are two ordinary systemd units you can stop and disable. --- # Installing Source: https://virtues.com/docs/setup/install One command, run on the machine that will become your server: ```bash curl -sSL https://virtues.com/sh | sudo sh ``` That fetches a small shell script, which downloads the installer for your architecture, verifies its checksum, and hands it off to the installer. Ten minutes later you have a working server. Before installing, read these two pages first: 1. [What to run it on](/docs/setup/requirements) for details on minimum specs and supported OS. 2. [Setting up inference](/docs/inference) to configure your embedding and rerank models. The installer asks for their URLs before it does anything else. ## Read it before you run it Piping a script into `sudo sh` deserves suspicion, so the script is deliberately tiny and readable: ```bash curl -sSL https://virtues.com/sh | less ``` It does five things: checks you're on Linux as root, resolves the newest stable release, downloads the installer binary and its `.sha256` sidecar, verifies the hash, and executes it. If the checksum can't be fetched or doesn't match, it refuses to run the binary rather than falling back on "HTTPS is probably fine" — a missing sidecar is a packaging bug, and this code is about to run as root. ## What it needs - **Linux**, `x86_64` or `aarch64`, with systemd and root. Not macOS — the Mac app is a client that talks to a server, not a server itself. - **Debian or Ubuntu**, where the installer drives `apt` and adds the PGDG repository if the distribution's own packages ship an older PostgreSQL than the one Virtues needs — or **Fedora**, where it drives `dnf` and takes PostgreSQL as shipped. - **About 4 GB free on `/`**, and `curl`. - **Outbound network** to `github.com` (releases) and `apt.postgresql.org` (PostgreSQL). The installer probes both before touching anything. Before it installs anything, it also measures the disk your record will live on and tells you, with numbers, which tier you're on — an NVMe drive and a microSD card produce very different servers, and that difference is worth knowing up front. [What to run it on](/docs/setup/requirements) has the full picture. It checks whether ports `5432`, `8000`, `18181`, and `18182` are already bound — Postgres, the server itself, and the two inference endpoints. A warning there usually means you're reinstalling over an existing server, which is fine. ## The first question is inference The installer asks how you want to run the two models that make your record searchable before it touches a package, a service, or a disk — so that a broken endpoint costs you a prompt rather than a half-finished install. On Virtues hardware this is pre-configured. On yours, you either point it at endpoints you already run, or take the bundled CPU-only trial, which is deliberately labeled as slow and not a deployment. If you choose your own endpoints, have them running before you start. [Setting up inference](/docs/inference) gives the commands, the models, and the contract those servers have to speak. To skip the prompt entirely on an unattended install, set `VIRTUES_INFERENCE` (with `VIRTUES_EMBED_URL`) in the environment. ## Channels The command above installs the newest **stable** release. To track prereleases instead: ```bash curl -sSL https://virtues.com/sh-pre | sudo sh ``` Or pin an exact version, stable or not: ```bash curl -sSL https://virtues.com/sh | sudo VIRTUES_VERSION=vX.Y.Z sh ``` The channel you install on is remembered, so later upgrades follow the same line without being asked again. See [Upgrading](/docs/operate/upgrading) for how a server moves between releases and how to roll one back. ## After it finishes The server comes up on port `8000`, and the machine is now a Virtues server, though note it holds nothing yet. The next step is pairing a device (which starts the flow of your own data). Those pages will land here in Docs soon. Until then, the installer's own output is the guide. ## Troubleshooting If anything looks wrong during CLI install, `virtues doctor` is the first thing to run. It reports how inference resolved, whether both endpoints are actually serving, and whether this server can be reached from away — each finding with the command that diagnoses it. [The CLI](/docs/operate/cli) covers the rest. Also note that `virtues --help` lists everything the CLI can do. --- # Reaching your server Source: https://virtues.com/docs/operate/reach Your server sits at home behind a router that, by default, nothing on the internet can reach. Getting to it from your kitchen and from another continent should feel the same, and it should not require opening a hole in your home network. This is how that works. ## The shape of it Every device and every server has its own cryptographic key. That key *is* its identity — there's no password, no account cookie, no bearer token to steal. Your phone dials your server by its key, and the server answers only devices whose keys are on its allowlist. Three paths, tried in order, all invisible to you: 1. **Direct on your own network.** Phone and server on the same Wi-Fi talk straight to each other. Nothing leaves the building. 2. **Direct across the internet.** The two ends punch through their routers and connect to each other without a middleman. 3. **Through a relay**, when the network won't allow a direct connection — some corporate and mobile networks won't. The connection upgrades itself to a direct path if one becomes possible. **No inbound port is ever opened at home.** The server dials out; nothing dials in. That's why this works on a normal home router with no configuration and no port forwarding. ## What you connect with The iPhone app and the desktop app. Both speak the key-based protocol; a plain web browser can't, because it has no key to prove it's yours — a browser pointed at the server on your own network is refused like any other stranger. So: the apps, or a terminal on the server itself. ## Pairing a device Pairing is putting a device's key on the server's allowlist. There are a few routes, and which one you take depends on where you are. **Your first phone, during setup**, connects over Bluetooth. The server shows a four-word phrase on its screen and the phone has to send it back before the server will do anything — proof you can see the machine, rather than proof you know a secret. **Any later device** pairs with a code from the server: ```bash virtues pair ``` That prints a code to type into the app, then waits. On a server that's already yours, each code is fresh and single-use. If your phone is on the same network as the server, the app can scan a QR instead. **A phone joining from a different network** can be handed its identity by a laptop that's already paired, from the Devices screen — one scan and it's in, with no network path needed between the two devices. > That handoff QR **contains a private key**. Anyone who photographs it while > it's on screen gets access to your server. Don't display it on a shared screen > or a video call, and if you suspect someone caught it, revoke the device > immediately — it appears in Devices the moment it pairs. To see and manage what's connected: ```bash virtues device ls # every device allowed to reach this server virtues device rm # revoke one; its next connection is refused ``` Revocation is immediate and total. The key stops working the next time it tries. ## What the relay can and cannot see When a direct path isn't possible, traffic passes through a relay we run. The honest description: - **It cannot read anything.** The connection is encrypted end-to-end between your device and your server, with keys the relay never holds. It forwards packets it has no ability to open. - **It does see** which two device keys are talking to each other, the IP addresses they connect from, and how much traffic passes and when. That's unavoidable for anything that forwards packets — a relay that couldn't see volume and timing couldn't move the bytes. - **It admits everyone.** The relay asks no one who you are: no account, no subscription, no sign-in. It defends itself with rate limits, not identity. Reachability is part of owning the server, not something you rent. We'd rather state that plainly than round it up to "blind." The strong claim — that we can't read your life — is true and rests on the encryption, not on promises about the relay's memory. Your server knows the relay's address out of the server, so it is reachable from your first boot with no account at all — the same way every connected product ships its rendezvous servers built in. It's a default, not a requirement: Settings → System → Network names the relay in use and carries the off switch, and a server with the relay off works normally on your own network. ## Checking the connection ```bash virtues doctor ``` Among other things this prints a reach summary: whether the server has an identity, whether it knows a relay or is local-network-only, and how many devices are paired. ## What to expect when the network changes Some behavior is worth recognizing so it doesn't read as a fault: - **After your phone sleeps, or moves between Wi-Fi and cellular**, the connection is rebuilt automatically. Usually a second or two; occasionally longer if it has to rebuild from scratch. - **A backgrounded phone deliberately parks its connection** so the radio can idle and the battery lasts. It isn't continuously connected, by design. - **A server that was just set up** can take a few minutes to become reachable from outside your home, as it settles into a relay. - **On restrictive networks** — some workplaces, some mobile carriers — the direct path fails and everything rides the relay. That's slower, and expected rather than broken. - **Guest and coworking Wi-Fi that isolates clients** blocks devices on the same network from seeing each other. The handoff QR above is the way through it. If the server itself seems unhealthy rather than unreachable, [When something breaks](/docs/operate/recovery) is the place to start. --- # Upgrading Source: https://virtues.com/docs/operate/upgrading Upgrades are one command, run on the server: ```bash sudo virtues upgrade ``` It needs root — the upgrade replaces the installed binary and restarts the service — and it's deliberately safe to run on a server you care about: the new release is downloaded and staged completely before anything changes, then activated in one atomic flip. The binary, the web app, and the actions runtime move together, so you can't end up with a UI newer than the server underneath it. A failure before the flip leaves the server exactly as it was; a failure after the flip rolls straight back. ## Channels There are two release channels: - **Stable** — `vX.Y.Z` releases. What a server should be on unless you've chosen otherwise, and the default for every command below. - **Prerelease** — the newest staging build, versioned like `vX.Y.Z-staging.N`. Explicit opt-in via `--pre`. If you've heard it called *edge* or *nightly*, this is the same thing. Your server remembers its channel, so a plain `sudo virtues upgrade` keeps following whichever line it's on. `--pre` is a one-off override, not a permanent switch. ## Checking before you leap ```bash sudo virtues upgrade --check ``` reports what's available without changing anything. Add `--pre` to check the prerelease line. This is the dry run — it tells you the version you'd get and whether the server considers itself ready to take it. ## Pinning a version ```bash sudo virtues upgrade --version v0.1.4 ``` installs a specific tag instead of the newest release. Resolution order is explicit: `--version` wins over `--pre`, which wins over the stored channel, which defaults to stable. ## Rolling back ```bash sudo virtues rollback ``` flips back to the previous release — a pure switch and service restart, with no database surgery. Releases are kept in slots on disk precisely so the last known-good one is still there when you want it. ## If upgrade says your install is too old Very old installs predate the slot layout the upgrader expects. The command will tell you, and the fix is the installer, which migrates in place: ```bash curl -sSL https://virtues.com/sh | sudo sh ``` --- # Backup & restore Source: https://virtues.com/docs/operate/backup-and-restore Your server holds things that exist nowhere else. This page is how you keep them when the hardware doesn't. Read the first section before the others. Backups here are encrypted to a key the server deliberately cannot keep, so the order of operations matters more than usual: get the key wrong and every archive you ever make is unreadable. ## First: mint the recovery key Backups are encrypted with [age](https://age-encryption.org), and your server stores only the *public* half — enough to write an archive, never enough to read one back. Create the keypair once: ```bash sudo virtues backup --init-key ``` This prints the secret key **once**, in a banner, and writes it nowhere. That is the whole design: an attacker who takes the server, and Virtues as a company, are equally unable to decrypt your archives. It also means the consequence lands entirely on you. **Put the key somewhere it will outlive the server, and make more than one copy.** A password manager, a printout in a drawer, a note in a safe — the failure to plan for is a house fire or a dropped laptop, not a burglar. There is no escrow, no "email me a reset", and no support path that recovers it. Losing this key turns every backup you own into noise. Until you run that command, `virtues backup` refuses to do anything. It used to mint a key automatically, which meant people ended up with archives whose key had scrolled off a terminal months earlier. ## Making a backup ```bash sudo virtues backup ``` Writes one self-contained encrypted archive to `/var/lib/virtues/backups/`, named for the moment it was taken and ending `.tar.gz.age`. Use `--output ` to put it somewhere else — an external drive you mounted by hand, for instance — and `--force` to overwrite an existing file. ### What's inside - The **database**, as a full dump. Your server's identity lives in here too — the network key that *is* this server, the certificate authority, the list of paired devices — so the archive carries who your server is as well as what it knows. - The **environment file**, which holds the encryption key that makes the database's stored credentials readable. Without it a dump restores into gibberish, which is why a backup refuses to run when it can't find one. - The **data lake** — recordings, uploads, files. - **Authored applets**, the ones written on the server rather than shipped with it. - A **manifest** recording the binary and schema versions, plus a SHA-256 for every member, which is what makes verification and restore able to detect a damaged archive. Because the environment file rides along, **the archive is exactly as sensitive as the server itself.** Treat a backup tarball the way you'd treat the machine. Deliberately *not* included: the downloaded model files, which are large and freely re-fetchable, and a handful of machine-local scraps like the saved Wi-Fi passphrase and the release channel. A restored server re-downloads and re-derives those. ## Backing up to a drive Plug in an external disk, mount it, and register it once: ```bash sudo virtues volumes add /path/to/mount --name "study drive" ``` The drive is remembered by its filesystem UUID rather than its mount path, so it's still recognized after a reboot moves it. Nothing outside the server's own subdirectory is ever read or written, so the disk stays usable for whatever else lives on it. ```bash virtues volumes ls # what's registered, and how fresh sudo virtues backup --volume all # write to every attached drive ``` **This already runs on its own.** A nightly job at 04:00 backs up to every registered drive that happens to be attached; drives that aren't plugged in are skipped quietly rather than failing. Registering a drive is, in practice, the entire setup. On a drive the archive is split: a full snapshot of the database, environment file and applets, plus separate incremental archives of the lake. Old full snapshots are pruned only when the drive starts filling up, and the newest is never removed — on a roomy disk you keep a run of them. Lake increments are never pruned at all, because the lake is the part that only ever grows and can't be re-derived. A run refuses to start if it would leave under a gigabyte free. ## Checking that a backup is real An unverified backup is a hope, not a plan: ```bash virtues backup --verify /path/to/archive.tar.gz.age --key-file /path/to/key ``` This decrypts the archive, extracts it, and re-hashes every file against the manifest. It writes nothing and doesn't need a working database, so it's safe to run against an old archive on a different machine — which is also the way to prove your saved key actually works before you need it to. Do this by hand from time to time. Nothing verifies archives on a schedule yet, so silent rot on an old drive would otherwise surface only during a restore. ## Restoring Restoring **replaces this server's state** with the archive's. It is not reversible and there is no dry run. ```bash sudo virtues restore /path/to/archive.tar.gz.age --key-file /path/to/key ``` Unlike backup, this runs as root. From a registered drive, pass the mount path — not the volume's name or id: ```bash sudo virtues restore --from-volume /path/to/mount --key-file /path/to/key ``` Before touching anything, restore checks that the service is stopped, reads the manifest, refuses an archive written by a *newer* release than the binary you're running, verifies every checksum, and proves it can reach the database. Then it gives you five seconds to press Ctrl-C. The version check and the checksums cannot be bypassed; if an archive came from a newer release, [upgrade](/docs/operate/upgrading) first and restore afterward. Restoring from a drive applies the newest full snapshot, then every lake increment in order. One caveat worth knowing: if an increment has been deleted from the drive, the restore proceeds with the ones it can see rather than stopping to tell you a window is missing. Verify the drive's contents before relying on a restore that matters. ## What this doesn't do yet Being precise about the edges, since backups are exactly the wrong place for optimism: - **There is no cloud or network destination.** "Off-server" today means a drive you physically plug in. Registering remote storage isn't implemented, so if your only copies live in the same building as the server, a fire takes both. - **Nothing escrows your recovery key.** It is a banner printed once; copies are entirely your responsibility. - **Nothing verifies archives on a schedule.** `--verify` is manual. - **The Settings panel is read-only.** It shows whether backups are current, stale, or failing, but there is no button — backup and restore are terminal verbs today. If you're moving to new hardware rather than recovering the same server, ask first: the archive carries the old server's identity, and what that means for your paired devices isn't settled yet. --- # When something breaks Source: https://virtues.com/docs/operate/recovery Start here when the server is misbehaving. You're reading this on the web rather than on the server for a reason: the machine that's broken is the worst place to keep its own troubleshooting guide. Almost everything below needs a terminal on the server, over SSH or at the keyboard. ## Is it running? ```bash sudo systemctl status virtues ``` The server runs as a systemd unit called `virtues`, restarts itself on failure, and waits for PostgreSQL before starting. If it's in a restart loop, the reason is in the log: ```bash sudo journalctl -u virtues -n 100 --no-pager ``` Everything logs to the journal. There is no Virtues log file to hunt for. A restart fixes more than it should: ```bash sudo systemctl restart virtues ``` ## What the server thinks of itself ```bash virtues status ``` Identity, subscription, and paired devices in one screen. When you want to hand someone the whole picture rather than describe it, `virtues status --json` prints the same thing in a stable form that's easy to paste. ```bash virtues doctor ``` Reports how the inference stack resolved on this hardware — which accelerator was found, whether this build links CUDA, and whether each model is present or would need downloading. It doesn't touch the database, so it still answers when other things are broken. These commands read the database, and the database belongs to the `virtues` service user. You don't have to think about that: run them as yourself and they re-launch themselves as the right user, printing a line to say so. ## When the server crashes If the server exits abnormally, it reports that to us: the exit status and the last 50 lines of the journal, so a crash on your server is something we can fix rather than something you have to notice and report. Nothing else on a running server is sent to us unprompted, apart from checking for updates. To turn the crash report off, set it in the server's configuration and restart: ```bash sudo sh -c 'echo VIRTUES_DIAG=off >> /var/lib/virtues/virtues.env' sudo systemctl restart virtues ``` Either way the journal itself is the full account, and it stays on your machine: ```bash sudo journalctl -u virtues -n 200 --no-pager ``` ## The pieces Beyond the main service, a server runs the inference sidecars — and on hardware with an NPU, one daemon replaces both: | Unit | What it is | |---|---| | `virtues` | the server itself, on port 8000 | | `virtues-embed` | embedding model, on local port 18181 | | `virtues-rerank` | reranking model, on local port 18182 | | `virtues-qnnd` | on NPU hardware, replaces both sidecars and serves both ports | | `virtues-display` | the on-server screen, if your server has one | If search returns nothing or feels broken while the server is otherwise healthy, suspect a sidecar: ```bash systemctl status virtues-embed journalctl -u virtues-embed -n 50 ``` **If the on-server screen shows an old version of the interface after an upgrade**, it's the kiosk holding a cached copy rather than anything deeper: ```bash sudo systemctl restart virtues-display ``` ## Where things live | What | Path | |---|---| | Everything the server owns | `/var/lib/virtues` | | Configuration and secrets | `/var/lib/virtues/virtues.env` | | Your files and recordings | `/var/lib/virtues/lake` | | Models | `/var/lib/virtues/models` | | Backups | `/var/lib/virtues/backups` | | Release channel | `/var/lib/virtues/channel` | | The binary | `/usr/local/bin/virtues` | The binary is a symlink into the currently active release, which is what lets an upgrade swap versions atomically and roll back with one flip. ## An upgrade went wrong The upgrade path is built so that failures before the switch leave the server untouched, and failures after it flip straight back. If you're on a release that's misbehaving: ```bash sudo virtues rollback ``` That returns the binary, the web app, and the actions runtime together. The database is not rolled back — migrations only move forward, and the previous release tolerates a newer schema. [Upgrading](/docs/operate/upgrading) has the full model. ## You can't reach the server If the server is healthy but your phone or laptop can't get to it, that's a different problem with its own page — see [Reaching your server](/docs/operate/reach). The short version: check that the device is still on the allowlist with `virtues device ls`, and re-pair with `virtues pair` if it isn't. ## Search results are wrong or empty If the server reports a model fingerprint or dimension mismatch — usually after changing models — the index was built by a different model than the one now answering: ```bash virtues configure-inference ``` To rebuild the index from your source data: ```bash virtues reindex ``` Your data isn't touched by either. The index is derived, so rebuilding it is recoverable by definition, just slow. ## Starting over `virtues restore` replaces the server's state from a backup. It's destructive and there's no dry run, so read [Backup & restore](/docs/operate/backup-and-restore) before reaching for it — particularly the part about needing the key you were shown once. To remove Virtues from the machine entirely, `sudo virtues uninstall` prints everything it found before touching any of it and asks you to type the server's hostname to confirm. --- # The CLI Source: https://virtues.com/docs/operate/cli Installing puts a `virtues` binary on the server. It's the same program that serves the web app, and it's how you do anything that needs to happen on the machine itself rather than through a paired device. `virtues --help` lists everything. This page covers the parts an owner actually reaches for; the rest is either service-internal or for people working on Virtues itself. ## Seeing what's going on ```bash virtues status ``` Server health in one screen: identity, subscription, and which devices are paired. When something is wrong and you want to hand someone the complete picture, `virtues status --json` prints the same thing in a stable machine-readable form — the boring-but-complete diagnostic. ```bash virtues doctor ``` Reports how the inference stack resolved on this hardware: which accelerator was detected, whether the build links CUDA, and whether each model is baked in or would be downloaded. It touches no database, so it answers the "is this machine set up right" question even when other things are broken. ## Devices Pairing a device is the one human verb for connecting something to your server: ```bash virtues pair ``` It prints a code to type into the app, then waits. On a server that's already yours each code is fresh, single-use, and good for thirty minutes — running this again mints a new one rather than reprinting the last, so use the code from the run you're looking at. (Only an unclaimed server, during setup, shows a standing code that pairs more than one device.) `login` and `link` still work as aliases. The devices allowed to reach your server are an explicit allowlist, and that allowlist *is* the authentication boundary: ```bash virtues device ls # who can reach this server virtues device add # print a pair code for a new one virtues device rm # revoke one — its next connection is refused ``` ## Approving sensitive actions A few actions are sensitive enough that a paired device can't do them alone: exporting all your data, swapping the key for a bring-your-own AI provider, wiping the server, revoking the last device other than itself, and importing an applet package. Asking for one from the app raises a request that you approve at the machine: ```bash virtues sudo ``` With no arguments it lists open requests and prompts for each. The point is physical access — someone who has your laptop can't approve from wherever they are, and you can, by sitting down at the server. ## Releases ```bash sudo virtues upgrade # move to the newest release on your channel sudo virtues rollback # flip back to the previous one virtues channel # print the channel this server follows virtues channel pre # follow prereleases from now on ``` Setting the channel persists it, which matters more than it sounds: `--pre` is a one-off override that forgets itself, so a server meant to track staging drifts back to stable the first time anyone types a bare `virtues upgrade`. Set the channel once instead. [Upgrading](/docs/operate/upgrading) covers the whole model, including pinning a version. If you'd rather split the work — do the slow download now, install later — `virtues prepare` stages and verifies a release without touching the running server, and `virtues activate` installs what it staged. ## Data ```bash sudo virtues backup # snapshot the server into one tarball sudo virtues restore # replace this server's state from one virtues volumes ls # registered backup destinations ``` The backup contains the encryption key needed to read its own contents, which makes the tarball **as sensitive as the server itself**. See [Backup & restore](/docs/operate/backup-and-restore) before you rely on any of this. ## Maintenance ```bash virtues reindex ``` Rebuilds the derived search index from your source data with the current model. Your data isn't touched — only the index built from it — so this is the recovery path for a stale or mismatched index rather than something to fear. ```bash virtues configure-inference ``` Re-validates the embedding endpoint after you change models, and offers to re-embed from source. Run it when the server reports a fingerprint or dimension mismatch. ## Leaving ```bash sudo virtues uninstall ``` Prints the exact manifest of everything it found before touching anything, and asks you to type the server's hostname to confirm. Shared infrastructure it didn't install — the PostgreSQL server, Avahi — is left alone. --- # Glossary Source: https://virtues.com/docs/understand/glossary Virtues names things deliberately, and uses one word per thing. This is that vocabulary, in roughly the order you meet it. ## The machine **Server** (or **server**) — the machine in your home running Virtues OS. It holds your data, does the thinking, and answers your devices. Nothing about it depends on us being reachable. **Device** — a phone or laptop paired with your server. Each one has its own key, and the server keeps a list of which keys are allowed. That list is the whole of authentication; there's no password to steal. See [Reaching your server](/docs/operate/reach). **Channel** — the release line your server follows, stable or prerelease. See [Upgrading](/docs/operate/upgrading). ## What comes in **Source** — somewhere your life is already recorded that the server can pull from: a calendar, a mail account, a health app, a bank, your phone's location. Connecting a source is what starts the flow. **Record** — one thing a source observed. A message sent, a place visited, a song played, a transaction, a heart rate sample, a recording. Records are the raw material and the server keeps them as they were; everything else is derived and can be rebuilt. **Lake** — where the large things live: recordings, uploads, files. The database holds the structure of your life, and the lake holds its bytes. ## What the server makes of it Everything below is *derived* from your records. It can be recomputed, which is why rebuilding an index or re-running a day is safe. **Event** — a bounded stretch of a day. A meeting, a drive, a walk, a meal. The server assembles events out of records that arrive incomplete, out of order, and sometimes contradicting each other. **Day** — the unit of narrative, and the one most worth reading. A day carries its own timeline of events and a written account of what happened, generated overnight from everything the server saw. **Article** — a page about a subject rather than a span of time. The people, places, and organizations in your record each get one, accumulating what's known about them. **Note** — a smaller observation attached to a subject: a correction, an appraisal, something worth remembering about it. **Ref** — a pointer from something the server wrote back to the record it came from. Refs are what make a claim checkable rather than merely plausible; when the server tells you something about your life, a ref is how you see why it thinks so. **Wiki** — where all of the above lands: the record's own encyclopedia. Days and their events on one axis, people, places, and organizations on the other, articles accumulating on both — and your chapters and narrative identity giving the whole thing its coordinates. ## What you author about yourself These are not derived and are never recomputed. The machine may write each one only while it is empty; from then on it is yours. **Narrative identity** — the account of who you are that grounds everything the models see: your history, in your own words, arranged from the interview and edited on its page from then on. **Chapter** — your own division of your life into named eras, rough years and all, given in the interview. Every day the record holds falls inside exactly one chapter; each chapter has a page of its own. ## What you make **Page** — a document you write, versioned as you go, and shareable. **Project** — a working lens over your life: a place to gather material around a question, with chat that's grounded in what you gathered rather than in everything. A project holds chats, pages and files, and never another project. ## What you can build **Applet** — a small program running on your server on a schedule or on demand: pulling from a source, computing something, keeping a record of its own. Some ship with Virtues, and some are written on the server — including by asking for them in chat. Each run is recorded, so an applet that quietly stops working is visible rather than silent. ## Words we're careful with **Observed, authored, generated** — the distinction between what a sensor recorded, what a person wrote, and what a model produced. It matters most in the last case: something the server generated is never treated as evidence on its own, which is why generated text carries refs back to records. **Derived** — anything the server computed and could compute again. Indexes, events, summaries. The opposite of your records, which are the only things that can't be regenerated and which backups exist to protect. See [Backup & restore](/docs/operate/backup-and-restore).