Deploy

July 28, 2026 ยท View on GitHub

Iva runs on one VPS as two systemd user services and five timers. install.sh sets all of it up (install); this page is what's actually running and how to operate it.

Transport: long polling

Telegram never connects to your server. scripts/telegram-poll.mjs long-polls getUpdates and POSTs each update to the local eve webhook (http://127.0.0.1:8723/eve/v1/telegram) with the shared X-Telegram-Bot-Api-Secret-Token header. Telegram sees an ordinary bot; the channel code is unchanged. No public HTTPS, no domain, no reverse proxy.

The bridge also gives you:

  • ๐Ÿ“ฌ Ordered delivery โ€” advances the offset (data/telegram-offset.json) only after eve replies 2xx, retrying with backoff up to 15s while the server boots.
  • โฑ Per-chat pacing โ€” a 1.5s pause between updates to the same chat, so a burst can't start two runs on one session.
  • ๐Ÿ›Ÿ Out-of-band recovery โ€” a handful of slash commands (/restart and friends) are handled by the bridge itself, so they work even when the agent is stuck. Which ones, and what they do: cli.md.

Webhook mode (alternative)

Polling and webhook are mutually exclusive โ€” the bridge calls deleteWebhook on start. If you do have a public HTTPS endpoint, disable the bridge and register the webhook:

systemctl --user disable --now iva-telegram-poll
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://<your-domain>/eve/v1/telegram",
       "secret_token":"'"$TELEGRAM_WEBHOOK_SECRET_TOKEN"'",
       "allowed_updates":["message","callback_query"]}'

Note: getUpdates โ€” which the setup wizard uses to discover your user ID โ€” stops working while a webhook is registered.

systemd units

bin/iva.mjs is the single source of truth for every unit. Any restart through the iva CLI regenerates them first, so Environment=PORT always matches IVA_PORT in .env. Don't hand-edit ~/.config/systemd/user/iva-* โ€” edits get overwritten. If you write your own unit instead, bake the port literally (Environment=PORT=8723): systemd will not expand $IVA_PORT from an EnvironmentFile.

The unit starts eve with --host 127.0.0.1, and a hand-written one must do the same. Setting HOST in the environment is insufficient because eve start overwrites HOST/NITRO_HOST for the process it spawns. Iva also requires the generated ASSISTANT_BEARER on Eve session routes. localDev() is included only under eve dev, which sets EVE_DEV=1; production does not use the client-controlled Host header as authentication.

The two controls cover different failure modes: loopback binding removes direct network reachability, while the bearer protects against another local process, SSRF, or a reverse proxy reaching the port. iva doctor repairs a missing bearer, .env permissions, and an old process still listening beyond loopback.

For a direct smoke test, load the secret without printing it and pass the header to curl through stdin rather than the process arguments:

ASSISTANT_BEARER="$(node --env-file=.env -p 'process.env.ASSISTANT_BEARER')"
IVA_PORT="$(node --env-file=.env -p 'process.env.IVA_PORT || 8723')"
printf 'header = "Authorization: Bearer %s"\n' "$ASSISTANT_BEARER" |
  curl --fail-with-body --config - \
  -X POST "http://127.0.0.1:${IVA_PORT}/eve/v1/session" \
  -H "content-type: application/json" \
  -d '{"message":"Reply with exactly: auth ok"}'
unset ASSISTANT_BEARER IVA_PORT
UnitWhenJob
iva.servicealwaysthe agent (eve start), Restart=always
iva-telegram-poll.servicealwaysthe long-polling bridge
iva-telegram-userbot.serviceopt-in (iva userbot setup)Telethon userbot MCP proxy โ€” see userbot.md
iva-memory-daily.timer04:00 nightlytranscript โ†’ cards + daily summary, report to Telegram
iva-memory-weekly.timerSun 04:157 dailies โ†’ weekly summary, report to Telegram
iva-memory-monthly.timer1st, 04:20weeklies โ†’ monthly summary (silent)
iva-memory-yearly.timerJan 1, 04:25monthlies โ†’ yearly summary (silent)
iva-memory-doctor.timer05:00 nightlyschema/health/decay/MOC checks + vault git push
iva-update-check.timer10:00 dailycheck for a newer stable Iva version; notify once per version

Memory timers fire in the server's local time. The update timer embeds ASSISTANT_TIMEZONE directly, so its 10:00 schedule remains correct even when the server clock uses UTC. All timers carry Persistent=true, so a run missed during downtime fires after reboot. Keeping the server clock aligned is still recommended:

sudo timedatectl set-timezone "$ASSISTANT_TIMEZONE"

Manual runs and status:

npm run memory -- daily   # or weekly | monthly | yearly
npm run doctor
systemctl --user list-timers
iva logs                  # agent; `iva logs poll` for the bridge

The update check fetches the configured Git upstream without calling the model. It stays silent when the installed stable version is current, when the same version was already offered, or when Telegram is not configured. A newer MAJOR.MINOR.PATCH release produces one message in TELEGRAM_DIGEST_CHAT_ID (falling back to the first trusted user) with Update and Later buttons. Errors are journal-only and retry on the next timer run.

Full CLI reference: cli. What the rollups actually write: memory.

eve's defineSchedule API (agent/schedules/*.ts) fires on self-host too: a built app served with eve start runs Nitro's schedule runner, so cron expressions tick inside the iva.service process (they need npx eve build + restart to register, and eve dev never fires them). Memory runs on systemd timers anyway โ€” they survive agent restarts and keep history in the journal โ€” but for reminders and recurring digests Iva can use either path: an eve schedule, or a plain crontab/systemd-run entry that sends via scripts/lib/telegram-send.mjs.

nginx and TLS

You need neither for Telegram - polling is outbound-only. If you expose the Telegram webhook, proxy only /eve/v1/telegram; that route verifies X-Telegram-Bot-Api-Secret-Token and the Telegram user allowlist.

Exposing the Eve HTTP channel is a separate security decision. Require HTTPS and preserve the Authorization: Bearer ... header so Iva can verify ASSISTANT_BEARER. Never remove the bearer check merely because the proxy connects to 127.0.0.1: loopback describes the proxy-to-Iva hop, not the original caller.

Moving servers

Your state is three things: the vault (its own git repo, pushed nightly by the doctor), .env (all keys), and data/ (tasks.json, usage.jsonl).

  1. Old box: npm run doctor to push the vault, then copy .env and data/ off.
  2. New box: run the installer (install) with --skip-setup, drop in .env.
  3. Clone the vault back โ€” gh repo clone <user>/iva-vault <vault-dir> โ€” restore data/, then iva restart.

If all you have left is the vault repo, you lose open tasks and token history. Memory survives intact.

Vercel (advanced)

Iva is built on eve, which deploys to Vercel natively โ€” but self-host is the intended path. If you go there anyway:

  • Schedules โ€” defineSchedule in agent/schedules/*.ts becomes a real Vercel Cron Job (cron times are UTC there).
  • Storage โ€” ./data is ephemeral on Vercel; tasks and usage logs need a real DB or KV store.
  • Auth - Eve routes accept Vercel OIDC or ASSISTANT_BEARER. localDev() is enabled only by eve dev; configure the bearer as a Vercel secret for any non-OIDC caller.