DocsDeploymentDistributed Mode
Back
Deployment

Distributed Mode

For teams that need a shared backlog, xpo can run in client-server mode. A headless server exposes the same data over HTTP with SSH key authentication.

Server setup

Start the server in a repo that has been initialized with xpo init:

xpo serve --addr :8080

The server handles authentication automatically on first run — no manual key setup needed.

Authorize team members

Add SSH public keys to .xpo/authorized_keys:

ssh-ed25519 AAAAC3Nza... alice@example.com
ssh-ed25519 AAAAC3Nza... bob@example.com

This file is committed to git so the team roster is version-controlled.

Client setup

xpo login https://xpo.example.com

This discovers your SSH keys, signs a challenge nonce, and caches a JWT locally. All subsequent commands work against the remote:

xpo list
xpo add "Remote issue" --label feature
xpo whoami
xpo logout

Web board

xpo board works in distributed mode — it runs the UI locally while proxying data requests to the remote server.

Docker deployment

docker build -t xpo:latest .
docker run -d \
  -v /path/to/repo:/data \
  -p 8080:8080 \
  xpo:latest

The container runs xpo serve by default.

Authentication flow

  1. Client calls POST /auth/challenge — server returns a 256-bit nonce.
  2. Client signs the nonce with an SSH private key and sends POST /auth/verify.
  3. Server validates against .xpo/authorized_keys and returns a JWT (7-day expiry).
  4. Client caches the JWT for subsequent requests.

Permissions

Configure role-based access in config.yaml:

permissions:
  roles:
    admin: ["*"]
    member: ["issue.read", "issue.create", "issue.update", "issue.comment", "issue.start"]
    viewer: ["issue.read"]
  users:
    alice@example.com: admin
    bob@example.com: member

Capabilities: issue.read, issue.create, issue.update, issue.comment, issue.start, issue.merge, issue.delete, issue.link.

When no permissions block is configured, all authenticated users have full access.

API endpoints

EndpointAuthDescription
POST /auth/challengeNoGet auth nonce
POST /auth/verifyNoExchange signed nonce for JWT
GET /healthzNoHealth check
/api/*JWTREST API
/mcpJWTMCP endpoint