# Self-Hosting Lycato

> Status: alpha, single tenant, source install. Lycato currently ships the local
> `lycato serve` supervisor, but it does not ship a Docker image, Compose file, or
> Caddy configuration. The public npm install is also blocked until the Lycato
> workspace packages are published together.

## Scope

This runbook covers one company repo on one trusted host:

- the built cockpit UI and API;
- periodic adapter sync;
- the minute-based agent scheduler;
- local backups and upgrades.

The cockpit has no application authentication. Keep it on loopback and use an
authenticated reverse proxy, SSH tunnel, Tailscale, or another private-network
control if it must be reached remotely.

## Prerequisites

- Node.js 22;
- pnpm via Corepack;
- Git;
- a Unix-like host for unattended operation.

## First boot from source

Build Lycato once, outside the company repo:

```bash
git clone https://github.com/axysar/lycato.git /opt/lycato
cd /opt/lycato
corepack enable
pnpm install --frozen-lockfile
pnpm build
```

Create a company repo in a separate directory. `init` is interactive and must
run inside the target directory:

```bash
mkdir -p /srv/lycato/company
cd /srv/lycato/company
node /opt/lycato/apps/cli/bin/run.js init
node /opt/lycato/apps/cli/bin/run.js doctor
```

Start the supervisor from that company repo:

```bash
cd /srv/lycato/company
node /opt/lycato/apps/cli/bin/run.js serve --interval 900
```

The cockpit and API share `http://127.0.0.1:3001/`. `lycato serve`:

1. starts the built cockpit UI, Fastify API, and WebSocket endpoint;
2. runs sync immediately and then at the requested interval;
3. checks scheduled agents every minute, replaying elapsed minute slots after
   timer drift or host suspension;
4. appends run evidence to `.founder/runs/YYYY-MM.jsonl`.

Use `--skip-cockpit`, `--skip-sync`, or `--skip-agents` when a separate process
owns that responsibility.

`daily-digest` does not write unattended company truth. Approve or deny its
action digest explicitly:

```bash
node /opt/lycato/apps/cli/bin/run.js agent approvals
node /opt/lycato/apps/cli/bin/run.js agent approve <digest>
```

## Running under a service manager

Use `/srv/lycato/company` as the working directory and this executable:

```text
/usr/bin/node /opt/lycato/apps/cli/bin/run.js serve --interval 900
```

Configure the service manager to restart on failure and stop it with `SIGINT`
or `SIGTERM`. Keep `.founder/keys.env` mode `0600`; Lycato loads it when the
supervisor starts.

## Upgrades

Stop the supervisor before upgrading:

```bash
cd /opt/lycato
git pull --ff-only
pnpm install --frozen-lockfile
pnpm build

cd /srv/lycato/company
node /opt/lycato/apps/cli/bin/run.js migrate
node /opt/lycato/apps/cli/bin/run.js doctor
```

Then restart the supervisor. Migrations are forward-only and idempotent.

## Backup and restore drill

```bash
node /opt/lycato/apps/cli/bin/run.js backup --out /backups/lycato-$(date +%F).lycatobak
node /opt/lycato/apps/cli/bin/run.js restore \
  --from /backups/lycato-YYYY-MM-DD.lycatobak \
  --to /tmp/lycato-restore-drill
```

The company directory is also a Git repo. A private remote is a useful second
backup tier, but it does not replace a tested `.lycatobak` restore.

## Operational checks

Run these from `/srv/lycato/company`:

```bash
node /opt/lycato/apps/cli/bin/run.js doctor
node /opt/lycato/apps/cli/bin/run.js logs --kind agent --since 2026-05-01
curl --fail http://127.0.0.1:3001/api/health
```

`/api/health` reports the repository root, bind host and port, authentication
mode, and network posture.

## Hardening

- Leave `LYCATO_COCKPIT_HOST` unset to retain the loopback-only default.
- If a non-loopback bind is unavoidable, put a real authentication boundary in
  front and set both `LYCATO_COCKPIT_HOST` and
  `LYCATO_COCKPIT_AUTH_MODE=reverse-proxy`. Lycato refuses the bind without that
  explicit declaration; it does not add the proxy boundary itself.
- Have that proxy strip any inbound `X-Lycato-Actor` and set it from the
  authenticated identity. Review receipts are bound to that actor when present,
  as well as to an HttpOnly same-site browser session, the branch, and the exact
  Git diff. Approval requires a fresh, single-use receipt from the diff route.
- The default CORS allowlist contains `https://lycato.app`,
  `https://www.lycato.app`, and the local Vite origin. Set
  `LYCATO_CORS_ORIGIN` to a comma-separated replacement allowlist when using a
  different frontend. The same-origin built UI is accepted automatically.
- Set `cockpit.live_updates: false` in `.founder/config.yml` only when the proxy
  cannot carry WebSocket upgrades.
- Run Lycato as an unprivileged account with write access only to the company repo
  and backup destination.

## Not included

The source-hosted path does not include Docker packaging, TLS automation,
passkeys/SSO, Stripe billing, KMS-backed secrets, multi-tenant orchestration, or
managed off-host backups. Those are separate hosted-product work, not implied
by this runbook.
