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 (
/restartand 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
| Unit | When | Job |
|---|---|---|
iva.service | always | the agent (eve start), Restart=always |
iva-telegram-poll.service | always | the long-polling bridge |
iva-telegram-userbot.service | opt-in (iva userbot setup) | Telethon userbot MCP proxy โ see userbot.md |
iva-memory-daily.timer | 04:00 nightly | transcript โ cards + daily summary, report to Telegram |
iva-memory-weekly.timer | Sun 04:15 | 7 dailies โ weekly summary, report to Telegram |
iva-memory-monthly.timer | 1st, 04:20 | weeklies โ monthly summary (silent) |
iva-memory-yearly.timer | Jan 1, 04:25 | monthlies โ yearly summary (silent) |
iva-memory-doctor.timer | 05:00 nightly | schema/health/decay/MOC checks + vault git push |
iva-update-check.timer | 10:00 daily | check 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).
- Old box:
npm run doctorto push the vault, then copy.envanddata/off. - New box: run the installer (install) with
--skip-setup, drop in.env. - Clone the vault back โ
gh repo clone <user>/iva-vault <vault-dir>โ restoredata/, theniva 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 โ
defineScheduleinagent/schedules/*.tsbecomes a real Vercel Cron Job (cron times are UTC there). - Storage โ
./datais 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 byeve dev; configure the bearer as a Vercel secret for any non-OIDC caller.