A modular, production-ready WhatsApp bot on zapo-js with a file-based command plugin system.
- Features
- Requirements
- Quickstart
- Pairing
- Configuration
- Commands
- Writing a plugin
- Project layout
- Troubleshooting
- Plugin architecture — drop a file into
commands/<category>/, it loads on boot with alias and cooldown support - Guard middleware —
ownerOnly,groupOnly,privateOnly,adminOnly,botAdminenforced before every execution - PN/LID aware — sender resolution uses
participantAlt/remoteJidAltfallback, so owner checks work in LID-addressed groups - Resilient session — SQLite credential store, QR rotation, 8-digit pairing codes, exponential-backoff reconnect with jitter
- Fast at scale — parallel plugin loading, LRU group-metadata cache with in-flight dedup, batched broadcasts, fire-and-forget read receipts
- Clean observability — zero-dependency pretty logger (
HH:MM:SS LVL [component] msg key=value) with level filtering
- Node.js
>= 20.9.0 - A WhatsApp account for linking (companion device)
cp .env.example .env
npm install
npm startFor live reload during development:
npm run devPairing defaults to an 8-digit code. Set your full international number in .env:
PAIRING_NUMBER=1551999999999The code is printed in the terminal — enter it via Linked devices → Link with phone number instead. While a code is active the QR is suppressed. If PAIRING_NUMBER is empty, the bot asks for it interactively; leave it blank (or if the code request fails) to fall back to QR.
QR fallback. A QR renders in the terminal — scan it from WhatsApp → Linked devices → Link a device before it expires.
Some accounts are passkey-gated by the server. The bot logs a warning in that case; link approval must happen on the primary device.
All settings live in .env:
| Key | Default | Description |
|---|---|---|
PREFIXES |
! |
Command prefixes, separated by comma or space |
SESSION_ID |
default |
Session key inside the store |
AUTH_PATH |
.auth/state.sqlite |
Credential and Signal storage |
BOT_NAME |
ZapoBot |
Name shown in help |
OWNER_NUMBERS |
— | Owner numbers, e.g. 1551999999999,1551888888888 |
PAIRING_NUMBER |
— | Phone number for code pairing |
LOG_LEVEL |
info |
Bot logs: trace, debug, info, warn, error |
ZAPO_LOG_LEVEL |
warn |
Library (socket/noise/auth) logs, kept quiet in prod |
MARK_ONLINE |
false |
Announce online on connect |
HISTORY_FULL_SYNC |
true |
Request full archive on pairing |
AUTO_READ |
true |
Send read receipts |
AUTO_TYPING |
true |
Show typing during commands |
WELCOME |
true |
Greet new group members |
MAX_RECONNECT |
20 |
Reconnect attempts before giving up |
MAX_REPLY |
0 |
Max eval/exec reply chars, 0 = unlimited |
HOT_RELOAD |
true |
Auto-reload plugins on file change |
| Category | Commands |
|---|---|
general |
ping, help, echo, info, id, runtime |
group |
tagall, hidetag, kick, add, promote, demote, subject, desc, open, close, link, leave |
owner |
eval, exec, logout, bc, reload |
tools |
calc, react |
Run !help <name> in chat for per-command usage. Owner shortcuts bypass the prefix: $ <shell> for exec, => <js> or > <js> for eval.
Create commands/<category>/<name>.js. Every field except name and execute is optional:
export default {
name: 'hello',
aliases: ['hi'],
description: 'say hello',
usage: 'hello <name>',
category: 'general',
cooldown: 3,
ownerOnly: false,
groupOnly: false,
privateOnly: false,
adminOnly: false,
botAdmin: false,
async execute(ctx) {
await ctx.reply(`hello ${ctx.text || ctx.pushName}`)
}
}The context (ctx) exposes:
| Field | Description |
|---|---|
client |
The WaClient instance |
event |
Raw incoming message event |
jid / senderJid / senderAltJid |
Chat, sender, and PN/LID alternate |
args / text / prefix / command |
Parsed invocation |
reply(content, opts) / send(content, opts) |
Quoted and plain sends |
react(emoji) |
React to the invoking message |
isGroup / isOwner |
Chat and owner flags |
registry / config |
Plugin map and bot config |
Target a user from a mention, reply, or number with resolveTarget(ctx) from src/services/group.js, and check adminship with isAdmin(client, groupJid, jid).
src/index.js bootstrap and shutdown hooks
src/config.js validated env config
src/logger.js pretty level-filtered logger
src/client.js WaClient factory
src/store.js SQLite store builder
src/auth.js QR and pairing-code flow
src/connection.js backoff reconnect loop
src/loader.js parallel plugin loader
src/context.js PN/LID-aware ctx builder
src/middleware.js permission and cooldown guards
src/events/message.js prefix router
src/events/group.js welcome handler
src/services/group.js cached metadata, admin checks, target parsing
src/utils/ jid, text, time, cooldown helpers
commands/ 23 plugins across general, group, owner, tools
| Symptom | Fix |
|---|---|
| QR expired | A fresh QR re-renders automatically; scan the latest one |
logged out on connect |
Session was unlinked — delete .auth/ and re-pair |
| Owner commands rejected | Check OWNER_NUMBERS uses full international format without + |
| Bot must be admin | Promote the bot in group settings first |
| High memory over time | Lower HISTORY_FULL_SYNC load or clear .auth/state.sqlite mailbox |
ISC. Built on zapo-js — see its docs for protocol details.