Skip to content

Quickstart

xixo is an indexer for personal data. You can try it on your own machine with ./dev, run its container image on a server, point an MCP client at it, or build on its GraphQL API. Pick the path that fits, and follow its link for the full guide.

The repository runs the whole stack in containers with ./dev. It needs Docker with Compose, Ruby, and a masks checkout beside it for sign-in.

Terminal window
git clone https://github.com/xixo/xixo.git
git clone https://github.com/masksrb/masks.git
masks/dev
xixo/dev

Rails, Vite, the worker, this site, and the three backing services all reload when you edit a file. xixo answers at http://xixo.localhost:8180, and this documentation at http://xixo.localhost:8181. You do not need to edit /etc/hosts, because *.localhost already resolves.

Terminal window
./dev # build and run in the foreground
./dev down # stop, keeping the volumes
./dev reset # delete the volumes and start over
./dev test # unit, server, client, and corpus suites

The other commands are logs, ps, psql, console, seed, corpus, reference, and fmt. ./dev help describes each one.

Port Service Override
8180 xixo, at http://xixo.localhost:8180 XIXO_PORT
8181 these docs XIXO_DOCS_PORT
8182 Vite XIXO_VITE_PORT
5434 Postgres XIXO_POSTGRES_PORT
9201 OpenSearch XIXO_OPENSEARCH_PORT
9000, 9001 MinIO and its console XIXO_MINIO_PORT, XIXO_MINIO_CONSOLE_PORT

The stack serves one tenant, xixo, on every host. ./dev --multi declares the tenants demo and acme instead, at http://demo.xixo.localhost:8180 and http://acme.xixo.localhost:8180. Each one signs in against the masks tenant of the same name. See tenants.

xixo has no accounts of its own. It signs people in through masks, which has to be running first. MASKS_ISSUER names the masks server and defaults to http://masks.localhost:12345. The masks dev stack’s setup token is masks-dev.

  1. Open http://xixo.localhost:8180 and click Connect a sign-in server.
  2. masks asks you to sign in with an account that is allowed to approve connections, and then to approve xixo. masks sends the client credentials to xixo directly, server to server.
  3. xixo then signs you in. On later visits the button reads Sign in.

The first time the stack starts, bin/rails db:prepare creates the database and runs db/seeds.rb. The seeds attach four resources to the tenant:

Default storage (database) Storage in Postgres, up to 64 MB per file. It is the default storage, so uploads go here unless the agent picks another place.
Object storage (items-xixo) A bucket in the stack’s MinIO. The seeds create the bucket.
Snapshots (web) Headless Chrome, which saves web pages as snapshots.
Local models (ollama) Ollama on the host at http://host.docker.internal:11434/v1, with a model for every role. It is the default inference resource.

To see them, click your avatar in the header to open Settings, then open Resources. Attach adds another resource, and attaching a resource describes each type. ./dev seed runs the seeds again without deleting anything.

The models run on the host, outside the containers. brew bundle installs Ollama. Pull the models that the seeds name:

Terminal window
ollama pull gemma3:4b
ollama pull llama3.1:8b
ollama pull qwen3:8b
ollama pull nomic-embed-text
Role Model
fast, vision gemma3:4b
smart llama3.1:8b
agent qwen3:8b
embedding nomic-embed-text

XIXO_INFERENCE_ORIGINS permits only http://host.docker.internal:11434, so an inference resource you attach in development has to use that origin. If Ollama was not running when the seeds ran, the seeds print a warning and xixo skips summaries until the resource answers. After you start Ollama, click Check on Local models. The check fails and names the missing models until each one is pulled.

Drop a file anywhere on the catalog. The upload returns as soon as the file is staged, and the worker does the rest. It analyzes the file, the agent connects it to tags and picks a resource that accepts it, and xixo writes the bytes there. The file’s page shows the analysis as it runs. xixo refuses an upload that no active resource can accept, and says why. See adding things and analysis.

xixo ships as a container image, ghcr.io/xixo/xixo. The same image runs the web process and the worker. It needs Postgres, OpenSearch, and a masks server to sign in against, and it migrates itself on boot.

Terminal window
docker pull ghcr.io/xixo/xixo:latest

See self-hosting for an example compose.yml, the secrets, tenants, and storage.

Point an MCP client that supports OAuth at /mcp:

Terminal window
claude mcp add --transport http xixo http://xixo.localhost:8180/mcp

xixo answers the first request with 401 and the protected-resource metadata, which names masks. The client registers with masks and requests a token itself, so this works only if the masks tenant allows dynamic client registration. The tools the client sees depend on the scopes it is granted. See connecting an agent and the MCP reference.

Everything the browser app does goes through /graphql. A page served by xixo uses its session cookie, and @xixo/client wraps that with typed documents for every operation the app uses:

Terminal window
npm install @xixo/client graphql
import { createXixo, metaCSRFToken, SearchDocument } from "@xixo/client";
const client = createXixo({ url: "/graphql", csrfToken: metaCSRFToken });
const { data } = await client.query(SearchDocument, { query: "lease" }).toPromise();

Any other client sends a masks access token as Authorization: Bearer. The token’s audience is the tenant’s origin followed by /mcp, the same token an MCP client holds:

Terminal window
curl https://xixo.example.com/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ search(query: \"lease\") { nodes { id title } } }"}'

See the GraphQL reference for every field and the scope it needs.