node bot.js and keep it running, it can run a Cloak bot.
The whole deployment
^20.19.0 || >=22.12.0. Nothing compiles at install time, so you need no Dockerfile, no compiler, no native addon and no glibc floor. The default wire is a plain wss:// WebSocket over the pure-JS ws package, and every dependency the SDK actually loads is pure JavaScript.
npm install does download two optional native extras (the WebTransport lane and the voice media engine) because they are declared as optionalDependencies. Nothing imports either one unless you opt in, so they cost disk, not compatibility. Use npm install --omit=optional to skip them.The host
Node 21.x never qualifies, and neither does Node 18. The HPKE crypto stack draws randomness from
globalThis.crypto.getRandomValues, which Node 18 leaves undefined in ESM. login() checks the runtime before it opens a socket and throws a CloakEnvironmentError with a remedy rather than letting you discover it in production. See When a bot will not start.
The token
The bot token has the shapebotid.tokenid.secret and grants full end-to-end encrypted membership of every server your bot is in. Anyone holding it can read those servers.
Read it from the environment, never from source:
The keystore has to survive restarts
This is the one deployment decision that can permanently break a bot. Your bot’s identity is published write-once on the server. If the keystore is lost after that publication, a restarted bot generates an identity the server will not accept, while members keep wrapping conversation keys to the old one. Decryption then fails forever until the bot’s identity is reset server-side. Two shapes are supported, and the deployment rule is the same for both: something durable has to hold it.- Writable disk. Point
keystorePathat a mounted volume, not at a container’s ephemeral filesystem. A redeploy destroys the ephemeral one. - No writable disk. Use
envKeystore({ onWrite })and persist the blob it hands you into your platform’s secret store. See Keystore backends.
.gitignore either way:
.gitignore
A production-shaped bot
Everything below is deployment plumbing rather than bot logic: a keystore on a durable path, anerror listener so nothing fails silently, a sendRejected listener because sends are fire-and-forget, and a signal handler that flushes the keystore on the way out.
bot.ts
Process management
The SDK owns reconnection. On an unexpected disconnect it recreates the transport, re-authenticates, and restores its lastwatch() target, backing off exponentially with jitter up to 30 seconds. Your supervisor does not need to help with that, and restarting the process to “fix” a disconnect only throws away warm keys.
What your supervisor is for:
- Restart on exit.
login()callsprocess.exit(1)in the sample above when the initial connect fails. A restart policy ofalwaysoron-failurecovers that. - Know that two failures stop the loop. A revoked token (
-2) and an SDK below the backend’s minimum wire version (-12) are terminal: the SDK emitsdisconnectand stops retrying, and the process stays alive doing nothing. Restarting will not help either one. If you would rather the supervisor notice, exit from thedisconnecthandler when the error carries one of those codes. See When a bot will not start. - Deliver
SIGTERMbeforeSIGKILL.destroy()flushes queued keystore writes. A hard kill can drop a key that just landed, which costs a re-acquire on the next start. - One instance per token. Two processes on the same token fight over the same session. If you need a rolling deploy, let the old instance exit before the new one logs in.
- Capture stdout. The SDK returns strings and never prints on its own, apart from two deliberate warnings about an unrecoverable keystore misconfiguration. Your logging is whatever your handlers write.
A systemd unit
/etc/systemd/system/cloak-bot.service
StateDirectory=cloak-bot gives you /var/lib/cloak-bot owned by the service user and surviving restarts, which is exactly what the keystore needs. /etc/cloak-bot.env holds CLOAK_TOKEN=... and should be mode 0600.
Platform notes
Anywhere without a writable volume, reach for
envKeystore instead of hoping the filesystem persists.
When Docker is worth it
For a text bot, Docker buys you nothing the platform’s stock Node image does not already give you. It is worth reaching for when you opt into one of the two native extras, because then the base image matters:
The SDK ships a
deploy/Dockerfile template for exactly those two cases. If you are not using either, deploy the Node process directly.
Two things any image you write has to get right:
Before you call it deployed
1
Confirm the keystore path is durable
Restart the process and check that the keystore file is still there with the same contents. If the second start logs a warning about a new identity, the path is ephemeral and you must fix it before the bot gets any real use.
2
Run the dependency report on the host
One command tells you the Node version the process really got, whether it satisfies
engines, and whether a secure RNG is reachable. See When a bot will not start.3
Attach an error listener
Without one, a handler that throws disappears. It changes no control flow, it only makes failures visible.
4
Check the token is not in the image or the repo
git log -p and docker history both remember.Next
Keystore backends
Files, environment variables, and writing your own adapter.
When a bot will not start
The dependency report, symptom by symptom.
Keys and the keystore
Why publication is write-once, and what the keystore holds.
Known limitations
What does not work yet, and what will not.