create-botmesh generates: the entry
file is src/bot.js, started with npm start
(node --env-file-if-exists=.env src/bot.js). Adjust the paths if yours differ.
If you write TypeScript, compile it first (tsc) and start the compiled file.
Option 1: a VPS with PM2
1. Prepare the server
2. First connection (WhatsApp on Baileys)
./sessions/<session>. As long as that folder
survives, the bot reconnects without a new scan. Telegram, Cloud API and the
other token-based channels skip this step.
3. Keep it running with PM2
pm2 restart and pm2 stop only shut the bot down cleanly if it handles the
signal: add the handler from Graceful shutdown (neither
the core nor the generated bot.js installs one). PM2 kills the process
kill_timeout ms after SIGINT (1600 ms by default), so keep --kill-timeout
longer than the drain timeout.
Option 2: Docker
Dockerfile
RUN corepack enable && pnpm install --prod --frozen-lockfile and copy
pnpm-lock.yaml instead of package-lock.json. pnpm 10 and later skip the
build script of better-sqlite3 until you approve it in pnpm-workspace.yaml
(allowBuilds: with better-sqlite3: true on pnpm 11, onlyBuiltDependencies
on pnpm 10; see SQLiteStore).
Add a .dockerignore with node_modules, sessions, data, .env and
*.qr.png, so credentials never end up in an image layer.
docker-compose.yml
SIGTERM on docker compose stop and waits stop_grace_period
before it kills the container: keep it longer than the drain timeout.
Environment variables
The framework reads no environment variable by itself: every credential and setting is a constructor option. Map your variables explicitly, for example:.env with node --env-file-if-exists=.env, and in production
take the values from your orchestrator or secret manager. Checking the required
ones at startup (like env() above) makes a missing secret fail with one clear
line instead of a provider error later.
Persistence
Sessions
The Baileys provider saves the WhatsApp credentials undersessionsDir/<session> (default ./sessions/<session>). Do not delete that
folder, or you will have to scan the QR again:
Store
The defaultMemoryStore lives in RAM. In production use SQLiteStore for
one process (or RedisStore for several replicas), so messages, chats,
contacts, flow checkpoints and AI history survive restarts:
file must exist (SQLite creates the file, not its folders).
With a persistent store, flow(...).persistent() checkpoints survive restarts.
See Persistent store.
Backups
Back up the session and the data together: they are what lets a redeployed bot resume without a new QR scan or lost conversations.sqlite3 data/bot.db ".backup backup.db" while it runs: with WAL on, a plain
copy of a live database can miss the latest writes. Take a backup before you
upgrade @botmesh/sqlite-store, because a file migrated to a newer schema
cannot be opened by an older release.
Graceful shutdown
bot.stop() disconnects the provider and ends active conversations. With
{ drain: true } it first waits for sends in flight, so a deploy does not
cut a reply in half:
Replies still being sent when
stop() begins, and those the provider already
accepted, are recorded in the message log (after-send hooks and the store
write, up to 5 s) before the store is closed, with or without drain.
After the drain nothing keeps running: messages still queued for a chat are
dropped, no new conversation starts until start(), paced replies still
waiting resolve with { skipped: true } and running ctx.stream()s are
cancelled without touching the transport again (they resolve with
cancelled: true). No timer of the bot keeps the process alive afterwards.
With durability, a message dropped this way is still in the
journal and is processed on the next start.
Health checks
A small HTTP endpoint tells Docker, Kubernetes or your load balancer whether the bot is alive and connected.bot.getConnectionState() is 'connecting',
'authenticating', 'ready' or 'closed':
- Use
/healthzfor liveness (restart the container when it fails) and/readyzfor readiness (send traffic only while it passes). - With replicas and an exclusive transport (Baileys), the standby
replicas are not connected on purpose, so their
/readyzanswers503. Do not restart them for it. - Bind the port to a private interface or protect it:
bot.metrics()holds no personal data, but it describes your traffic. @botmesh/serverserves/healthzand/readyznext to the webhook routes: see Webhook server.
Logging
Withoutlogger, the bot logs through a built-in pino logger at warn, so
warnings from plugins and handlers (for example a failed AI reply) are visible,
and debug: true lowers it to debug. In production, pass your own logger to
choose the level and the destination:
logIds: 'raw' for local debugging only.
Next steps
- Going to production: the full checklist
- Running N replicas: several processes of one bot (failover, webhook throughput)
- Operations: metrics and events
- FAQ: common problems