Skip to main content
Production runs on a single VPS behind nginx, two pm2 processes:

CI/CD

Push to main triggers .github/workflows/cd.yml:
  1. verify job (GitHub-hosted Ubuntu): pnpm install --frozen-lockfile, then pnpm typecheck and pnpm lint scoped to runtime packages (skips @signalium/contracts which needs Foundry and @signalium/subgraph which needs graph-cli codegen).
  2. deploy job: SSHes to the VPS using a deploy-only ed25519 key, runs git fetch && reset --hard origin/main, pnpm install --frozen-lockfile --prefer-offline, pnpm build --filter=@signalium/web --filter=@signalium/api, then pm2 reload ecosystem.config.cjs --update-env.
End-to-end: ~1.5 min from push to live.

Required GitHub Actions secrets

First-time bootstrap

Walks through cloning, env setup, pm2 init, nginx vhosts, and certbot. Lives in deploy/README.md. Highlights:

pm2 ecosystem

ecosystem.config.cjs declares both processes. Key gotcha: interpreter: 'none' is required so pm2 doesn’t try to fork the pnpm shell wrapper as a Node script.

SQLite cache

The API’s SQLite db lives at apps/api/data/signalium.db and holds the X post metadata cache, AI proposals (keyed by judgeStorageRoot), and the 0G/USD price cache. The CD workflow never touches this directoryapps/api/data/ is gitignored, so production state is preserved across deploys. If the production DB needs a backfill (e.g., proposals created locally need to flow to prod), checkpoint the local WAL with sqlite3 apps/api/data/signalium.db "PRAGMA wal_checkpoint(TRUNCATE);" and scp the file after stopping the API process.