Skip to main content
Level: Advanced · Tags: Production, Docker, SQLite, Durability, WhatsApp Features shown: Persistent store (SQLiteStore, maxMessagesPerChat) · Durable message processing (store.durability()) · Persistent flows (.persistent(), .timeout) · Graceful shutdown (bot.stop({ drain })) · Health checks and metrics (toPrometheus, bot.metrics()) · Logging (pino, logIds) · Operations events · Going to production

Prerequisites

Node.js 22.12 or newer, as in Getting started. Docker is optional.

Project layout

The complete bot

The project files

package.json (npm install adds the dependencies):

How it works

1. One SQLite file for all state

SQLiteStore keeps messages, chats, contacts, user data and flow checkpoints. Its folder must exist (SQLite creates the file, not the folders), hence the mkdirSync. maxMessagesPerChat bounds the message log, which is unbounded by default on persistent stores.

2. Durability

With durability, every admitted message is written to an inbox journal before any handler runs, and every reply to an outbox under an idempotency key before the provider is called. If the process dies halfway, the next start replays the message and skips the replies that already went out. store.durability() keeps both journals in the same file. Keep instanceId stable across restarts of one deployment.

3. Logs without personal data

The bot logs through the pino logger you pass. Chat and sender ids are hashed in the bot’s own log lines (logIds: 'hash', the default). Your own lines should follow the same rule: step 5 logs the flow name, the outcome and the duration, never the text or the phone number. LOG_LEVEL=debug shows more while you debug.

4. Persistent flows

.persistent() saves the flow’s position and c.state in the store after each step, so a user halfway through ticket continues after a deploy. The sample transcript below restarts the bot in the middle of the flow.

5. Events for operations

'conversation:ended' reports every flow with its outcome (completed, cancelled, timedOut…). A .persistent() flow interrupted by a restart reports cancelled and then continues from the store, so do not alert on cancelled alone. 'error' receives handler failures; send them to your error tracker. See Operations for the rest.

6. Health and metrics

/healthz answers while the process serves (liveness: restart the container when it fails). /readyz answers 200 only while the bot is connected and not stopping (readiness). /metrics is bot.metrics() in Prometheus text format. Keep the port private: the compose file binds it to 127.0.0.1.

7. Graceful shutdown

On SIGTERM (Docker, Kubernetes) or SIGINT (Ctrl+C, PM2), bot.stop({ drain: true, timeout: '15s' }) waits for the replies in flight, records them and closes the store; then the health server closes and nothing keeps the process alive. A second signal waits for the same shutdown. Keep the orchestrator’s grace period (stop_grace_period) longer than the drain timeout.

8. One file, three ways to run it

npx botmesh dev src/bot.js imports only the factory and injects the terminal simulator, so you try the same bot (with the same SQLite file) without a phone. node src/bot.js and the container run the isMainModule block.

Sample transcript

Captured with LOG_LEVEL=silent npx botmesh dev src/bot.js; with the default info level, the conversation ended log lines of step 5 appear between the replies. :reset starts a fresh bot, as a restart or a deploy would, and the flow continues from the store:

Run it on WhatsApp

Locally: npm start, scan the QR with the phone that will be the bot, then press Ctrl+C. With Docker, create the volume folders first (owned by your user, uid 1000 like the image’s node user), then follow the logs to scan the QR the first time:
For Telegram, install @botmesh/telegram and build the bot with new TelegramProvider({ token: process.env.TELEGRAM_TOKEN }) instead.

Next steps