Skip to content

Latest commit

 

History

History
741 lines (530 loc) · 23.4 KB

File metadata and controls

741 lines (530 loc) · 23.4 KB

Self-Hosting Veritas Kanban

This guide walks you through every self-hosting scenario — from running locally for personal use to a production deployment behind a reverse proxy or on Tailscale.

Credit: This guide was originally contributed by @xechehot in PR #126. It has been expanded here to cover additional deployment patterns.

Remote security posture: v5 remote access is designed around a trusted same-origin host for the web client, /api, and /ws. Before exposing Veritas beyond loopback, read ADR 0002: v5 Remote Server Mode and Network Security Posture.


Table of Contents


Prerequisites

Requirement Version Install
Node.js 22.0.0+ https://nodejs.org or nvm install 22
pnpm 11.1.1+ corepack enable && corepack prepare pnpm@11.1.1 --activate
Git any https://git-scm.com

Verify:

node --version   # v22.x.x
pnpm --version   # 11.x.x

Build Steps

# 1. Clone the repository
git clone https://github.com/BradGroux/veritas-kanban.git
cd veritas-kanban

# 2. Install all workspace dependencies
pnpm install --frozen-lockfile

# 3. Build all packages (shared → server + web)
pnpm build

# 4. Configure environment
cp server/.env.example server/.env
# Edit server/.env — at minimum set VERITAS_ADMIN_KEY

The build produces:

Path Contents
shared/dist/ Shared TypeScript types and utilities
server/dist/ Compiled Express API server
web/dist/ Static React frontend (served by Express in production)

Local Hosting

Run Veritas Kanban on your own machine for personal use.

# Start the production server
NODE_ENV=production node server/dist/index.js

The app is now available at http://localhost:3001.

  • API: http://localhost:3001/api
  • UI: http://localhost:3001
  • WebSocket: ws://localhost:3001/ws
  • API Docs (Swagger): http://localhost:3001/api-docs

The example env file keeps VERITAS_AUTH_LOCALHOST_BYPASS=true for local development convenience, so unauthenticated loopback requests can use the configured local role outside production. In NODE_ENV=production, the server does not honor localhost bypass for HTTP or WebSocket auth. Set VERITAS_AUTH_LOCALHOST_ROLE=admin only for trusted local development when you want full access without a key on your own machine.

Development mode

For active development with hot-module replacement:

pnpm dev
# Vite dev server → http://localhost:3000 (proxies API to :3001)
# Express API server → http://localhost:3001

Note: NODE_ENV=development is for local development only. Never use it in Docker — the Express server does not serve the frontend in development mode.


LAN Access

Serve Veritas Kanban to other devices on your local network (phones, other laptops, tablets).

LAN access is server mode, not localhost mode. Keep VERITAS_AUTH_ENABLED=true, set VERITAS_AUTH_LOCALHOST_BYPASS=false, and use HTTPS, VPN, or a trusted tunnel for browser/mobile sessions unless the network is strictly private and temporary. Unauthenticated /metrics scrapes are allowed only when PROMETHEUS_METRICS_PUBLIC=true; otherwise use PROMETHEUS_METRICS_TOKEN or an API key with telemetry read access.

1. Bind to all interfaces

By default the server listens on 127.0.0.1. To accept connections from other devices, set HOST=0.0.0.0:

HOST=0.0.0.0 NODE_ENV=production node server/dist/index.js

Or add to server/.env:

HOST=0.0.0.0

2. Allow Vite dev server (development only)

If you're running the Vite dev server in development mode, set VITE_ALLOWED_HOSTS so Vite accepts requests from your LAN IP:

# server/.env or export before running pnpm dev
VITE_ALLOWED_HOSTS=192.168.1.100,my-machine.local
# Or allow all hosts (development only — never in production):
VITE_ALLOWED_HOSTS=*

This controls vite.config.ts's server.allowedHosts setting.

3. Update CORS

Add your LAN IP or hostname to CORS_ORIGINS in server/.env:

CORS_ORIGINS=http://localhost:3000,http://localhost:5173,http://192.168.1.100:3001

4. Find your LAN IP

# macOS / Linux
ip route get 1 | awk '{print $7; exit}'
# or
hostname -I | awk '{print $1}'

# macOS
ipconfig getifaddr en0

Your LAN URL will be http://<your-ip>:3001.


Tailscale Serve

Tailscale Serve lets you expose Veritas Kanban securely to your tailnet (all your devices) without opening firewall ports. This was the primary use case from @xechehot's original PR #126.

Treat the Tailscale URL as a remote origin. Do not rely on localhost bypass for tailnet clients, and verify /api, /ws, /health/ready, and /api/auth/status through the served HTTPS URL.

Option A: Root path (simplest)

Expose the app at https://<your-machine>.ts.net/:

# Start Veritas Kanban
NODE_ENV=production node server/dist/index.js

# Expose via Tailscale Serve (proxies HTTPS → localhost:3001)
tailscale serve https / http://localhost:3001

Access from any tailnet device at https://<your-machine>.ts.net.

Option B: Sub-path routing (/kanban/)

If you want to share port 443 with other services and serve Veritas Kanban under /kanban/:

Step 1 — Build the frontend with the base path

The frontend must be built with VITE_BASE_PATH=/kanban/ so all asset URLs, API calls, and client-side routes use the correct prefix:

VITE_BASE_PATH=/kanban/ pnpm --filter @veritas-kanban/web build
# Then rebuild the server (if needed)
pnpm --filter @veritas-kanban/server build

Or with Docker:

docker build --build-arg VITE_BASE_PATH=/kanban/ -t veritas-kanban .

Step 2 — Start the server

NODE_ENV=production node server/dist/index.js

Step 3 — Configure Tailscale Serve

# Route /kanban/ traffic to localhost:3001
tailscale serve https /kanban/ http://localhost:3001

# Verify the serve config
tailscale serve status

Step 4 — Update CORS

CORS_ORIGINS=https://<your-machine>.ts.net

Access the app at https://<your-machine>.ts.net/kanban/.

Tailscale Funnel (public internet access)

To expose beyond your tailnet (public internet):

tailscale funnel 443 on

Security: Funnel makes your instance publicly accessible. Ensure VERITAS_AUTH_ENABLED=true and use a strong VERITAS_ADMIN_KEY before enabling Funnel.


Reverse Proxy

For production deployments with TLS, use a reverse proxy in front of Veritas Kanban. Always set TRUST_PROXY when behind a proxy.

The supported reverse-proxy shape is same-origin: the proxy serves the app shell, API, WebSocket, health endpoints, and future PWA assets from the same public origin. Split-origin setups require exact CORS_ORIGINS, WebSocket upgrade testing, and explicit token handling as described in ADR 0002.

nginx

# server/.env
TRUST_PROXY=1
CORS_ORIGINS=https://kanban.example.com
upstream veritas {
    server 127.0.0.1:3001;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name kanban.example.com;

    ssl_certificate     /etc/letsencrypt/live/kanban.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/kanban.example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Proxy everything to Veritas Kanban
    location / {
        proxy_pass http://veritas;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket upgrade (real-time updates)
    location /ws {
        proxy_pass http://veritas;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        # Keep WebSocket connections alive
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name kanban.example.com;
    return 301 https://$server_name$request_uri;
}

Reload nginx after editing:

sudo nginx -t && sudo systemctl reload nginx

Caddy

Caddy handles TLS automatically (no certificate config needed):

kanban.example.com {
    reverse_proxy localhost:3001
}
# server/.env
TRUST_PROXY=1
CORS_ORIGINS=https://kanban.example.com

Caddy handles WebSocket proxying and HTTP→HTTPS redirects automatically.

Sub-path with nginx

To serve under /kanban/ on a shared domain, build with VITE_BASE_PATH and strip the prefix in nginx:

VITE_BASE_PATH=/kanban/ pnpm --filter @veritas-kanban/web build
pnpm --filter @veritas-kanban/server build
location /kanban/ {
    proxy_pass http://127.0.0.1:3001/;  # trailing slash strips the prefix
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location /kanban/ws {
    proxy_pass http://127.0.0.1:3001/ws;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 86400s;
}

Docker

Docker is the recommended approach for production deployments.

Do not use the demo Compose files (docker-compose-demo.yml or demo/docker-compose.demo.yml) for production or shared-network deployments. They are local demo configs and may disable auth for convenience. Use the authenticated production compose example below and generate fresh secrets.

Quick start

# Clone and configure
git clone https://github.com/BradGroux/veritas-kanban.git
cd veritas-kanban
cp server/.env.example server/.env
# Edit server/.env — set VERITAS_ADMIN_KEY to a strong secret (≥ 32 chars)

# Build and start
docker compose up -d --build

# Verify
curl http://localhost:3001/health
# → {"status":"ok","timestamp":"..."}

The app is available at http://localhost:3001. Data persists in a named Docker volume (kanban-data).

docker-compose.yml

services:
  veritas-kanban:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: veritas-kanban
    ports:
      - '3001:3001'
    environment:
      - NODE_ENV=production
      - PORT=3001
      - DATA_DIR=/app/data
      - VERITAS_ADMIN_KEY=your-secure-admin-key-here # ≥ 32 chars
      - VERITAS_JWT_SECRET=your-jwt-secret-here # prevents session resets on restart
      # - CORS_ORIGINS=https://kanban.example.com
      # - TRUST_PROXY=1                               # if behind nginx/Caddy/Traefik
      # - VERITAS_API_KEYS=agent1:key1:agent,readonly:key2:read-only
    volumes:
      - kanban-data:/app/data
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3001/health']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

volumes:
  kanban-data:
    driver: local

Building with a sub-path

docker build --build-arg VITE_BASE_PATH=/kanban/ -t veritas-kanban .

Common Docker commands

docker compose up -d --build     # Build and start in background
docker compose logs -f           # Follow logs
docker compose down              # Stop and remove containers
docker compose pull              # Pull latest image (if using a registry)

# Inspect health status
docker inspect --format='{{.State.Health.Status}}' veritas-kanban

Data persistence

The DATA_DIR=/app/data volume holds all persistent data:

/app/data/
├── tasks/
│   ├── active/        # Active task markdown files
│   └── archive/       # Archived tasks
└── .veritas-kanban/
    ├── config.json    # App settings
    ├── security.json  # JWT secret (if VERITAS_JWT_SECRET not set)
    └── logs/          # Application logs

Without a named volume, data is lost on every docker compose down. Always use a volume or bind mount.


Security

Generate strong keys

# Admin key (≥ 32 chars)
openssl rand -hex 32

# JWT secret
openssl rand -hex 64

Minimum production config

VERITAS_AUTH_ENABLED=true
VERITAS_ADMIN_KEY=<output of openssl rand -hex 32>
VERITAS_JWT_SECRET=<output of openssl rand -hex 64>
VERITAS_AUTH_LOCALHOST_BYPASS=false

When exposing Veritas beyond loopback, also use NODE_ENV=production and keep the web client, /api, /ws, health endpoints, PWA assets, and service-worker scope on one trusted origin whenever possible. Split-origin deployments must set exact CORS_ORIGINS entries and confirm WebSocket origin/upgrade behavior. Prometheus /metrics fails closed outside explicit loopback development binds; use PROMETHEUS_METRICS_TOKEN or PROMETHEUS_METRICS_PUBLIC=true for external scrapers. Mobile install steps and offline-shell behavior are documented in PWA install.

API keys for agents

Grant agents scoped access without giving them the admin key:

# Format: name:key:role,...
# Roles: admin | agent | read-only
VERITAS_API_KEYS=my-agent:vk_abc123:agent,dashboard:vk_xyz456:read-only

Role permissions:

Role Access
admin Full access to all endpoints
agent Read/write tasks, run agents, manage worktrees
read-only GET endpoints only (view tasks, read config)

Authentication methods

# Bearer token
curl -H "Authorization: Bearer <api-key>" http://localhost:3001/api/tasks

# X-API-Key header
curl -H "X-API-Key: <api-key>" http://localhost:3001/api/tasks

# Query parameter (WebSocket)
wscat -c "ws://localhost:3001/ws?api_key=<api-key>"

HTTP requests must pass API keys in Authorization: Bearer <key> or X-API-Key. Do not put long-lived credentials in HTTP query strings, bookmarks, QR codes, screenshots, reverse-proxy logs, or shareable URLs. The WebSocket api_key query fallback exists for clients that cannot set headers during the upgrade.

TRUST_PROXY

Always set TRUST_PROXY=1 when running behind a reverse proxy. Without it, Express uses the proxy's IP for rate limiting instead of the real client IP, so all users share one rate limit bucket.

TRUST_PROXY=1      # One proxy hop (nginx, Caddy directly in front)
TRUST_PROXY=2      # Two hops (CDN + reverse proxy)
TRUST_PROXY=loopback  # Only trust loopback (127.0.0.1, ::1)

Warning: TRUST_PROXY=true is blocked by default — it trusts all proxies and is unsafe on the public internet. Use a hop count or subnet instead.


Environment Variables Reference

All variables live in server/.env (copy from server/.env.example).

Server

Variable Default Description
PORT 3001 HTTP server port
HOST 127.0.0.1 Bind address. Set 0.0.0.0 for LAN/container access
NODE_ENV production for production. Never development in Docker
LOG_LEVEL info trace / debug / info / warn / error / fatal

Authentication

Variable Default Description
VERITAS_AUTH_ENABLED true Enable authentication. Set false only for trusted local use
VERITAS_ADMIN_KEY Admin API key. Must be ≥ 32 chars. Required for production
VERITAS_API_KEYS Additional keys. Format: name:key:role,name2:key2:role2
VERITAS_JWT_SECRET auto-gen JWT signing secret. Unset = auto-generated (sessions reset on restart)
VERITAS_AUTH_LOCALHOST_BYPASS false Allow unauthenticated localhost requests
VERITAS_AUTH_LOCALHOST_ROLE read-only Role for localhost bypass: read-only, agent, or admin

Networking

Variable Default Description
CORS_ORIGINS http://localhost:3000,... Comma-separated allowed CORS origins
TRUST_PROXY Express proxy trust. Use 1 for single-hop (nginx/Caddy). true is blocked
RATE_LIMIT_MAX 300 Max API requests/minute/IP (localhost exempt)

Prometheus metrics

GET /metrics is public only in local development. In production, scrape it with one of these explicit configurations:

scrape_configs:
  - job_name: veritas-kanban
    metrics_path: /metrics
    static_configs:
      - targets: ['veritas.example.com']
    bearer_token: '<PROMETHEUS_METRICS_TOKEN>'

Set PROMETHEUS_METRICS_TOKEN on the Veritas server to the same secret, or use a normal Veritas API key with telemetry:read permission in the Authorization: Bearer <key> header. Only set PROMETHEUS_METRICS_PUBLIC=true for trusted private networks where unauthenticated metrics are intentional.

Data & Storage

Variable Default Description
VERITAS_DATA_DIR .veritas-kanban Config, logs, internal state (relative to project root)
DATA_DIR /app/data (Docker) Mapped data dir inside Docker container
TELEMETRY_RETENTION_DAYS 30 Days to keep telemetry event files
TELEMETRY_COMPRESS_DAYS 7 Days after which telemetry files are gzip-compressed

Frontend (build-time)

Variable Default Description
VITE_BASE_PATH / Sub-path prefix for the frontend (e.g., /kanban/). Set at build time, not runtime
VITE_ALLOWED_HOSTS Comma-separated hostnames allowed by Vite dev server (dev only). * allows all

Integration

Variable Default Description
CLAWDBOT_GATEWAY http://127.0.0.1:18789 OpenClaw gateway URL for AI agent orchestration
VERITAS_WEBHOOK_URL Push task/chat events to an external service
VERITAS_WEBHOOK_SECRET HMAC-SHA256 secret for webhook payload signing

Troubleshooting

Cannot GET / (UI not loading)

Cause: NODE_ENV=development is set in Docker. In dev mode, Express is API-only — it does not serve the frontend.

Fix: Remove NODE_ENV=development from your Docker environment. The Dockerfile defaults to production.


CORS error in browser console

Cause: Your frontend origin is not in CORS_ORIGINS.

Fix: Add the exact origin (scheme + hostname + port) to CORS_ORIGINS:

CORS_ORIGINS=https://kanban.example.com,http://192.168.1.100:3001

No trailing slashes. The origin must match exactly what the browser sends in the Origin header.


WebSocket connection fails or disconnects immediately

  1. Check that your reverse proxy forwards WebSocket upgrade headers:
    • nginx: Needs proxy_set_header Upgrade $http_upgrade; Connection "upgrade"; in the /ws location block.
    • Caddy: Handles WebSocket automatically — no config needed.
  2. Check proxy timeout: WebSocket connections are long-lived. Set proxy_read_timeout 86400s in nginx.
  3. Verify CORS_ORIGINS includes the WebSocket origin.

Assets 404 after sub-path deployment

Cause: Frontend was built without VITE_BASE_PATH.

Fix: Rebuild with the correct base path:

VITE_BASE_PATH=/kanban/ pnpm --filter @veritas-kanban/web build

Or with Docker:

docker build --build-arg VITE_BASE_PATH=/kanban/ -t veritas-kanban .

Rate limiting errors (ERR_ERL_UNEXPECTED_X_FORWARDED_FOR)

Cause: Your reverse proxy sends X-Forwarded-For but TRUST_PROXY is not set.

Fix:

TRUST_PROXY=1

Tailscale: "ERR_TOO_MANY_REDIRECTS" or assets not loading

Cause: VITE_BASE_PATH not set when using Tailscale Serve with sub-path routing.

Fix: Rebuild with VITE_BASE_PATH=/kanban/ (see Tailscale Serve above).


Weak admin key warning at startup

The server rejects or warns on VERITAS_ADMIN_KEY shorter than 32 characters. Generate a proper key:

openssl rand -hex 32

Sessions reset after container restart

Cause: VERITAS_JWT_SECRET is not set, so a new secret is generated each startup.

Fix: Set a persistent JWT secret:

openssl rand -hex 64
# Add to docker-compose.yml environment or server/.env
VERITAS_JWT_SECRET=<output>

Check server health

curl http://localhost:3001/health
# → {"status":"ok","timestamp":"..."}

Auth diagnostics (requires admin key):

curl -H "X-API-Key: your-admin-key" http://localhost:3001/api/auth/diagnostics

For general deployment (Docker, bare metal, systemd, reverse proxy) see also docs/DEPLOYMENT.md.