Skip to main content
The keystore holds your bot’s published identity, every conversation key it has acquired, the peer identity keys it has pinned, its direct-message keys, and any voice key it has been sealed. Keys and the keystore covers what those are and why losing them is unrecoverable. This page is about where the bytes go. The SDK owns the keystore’s JSON shape. A backend owns only storage: read a string, write a string. That split is what makes a host with no writable volume serviceable.

Choosing a backend

keystorePath and keystore are mutually exclusive. Passing both throws.

The file store

keystorePath is shorthand for keystore: fileKeystore(path), and you can pass the adapter directly if you prefer to be explicit:
What the file store gives you:
  • Atomic writes. Contents go to a .tmp sibling in the same directory and are renamed over the target. A crash, a full disk, or a power loss mid-write leaves either the intact previous file or the intact new one, never a truncated keystore.
  • 0600 permissions. Only the owning user can read it.
  • A missing file means first run. Any other read failure (EACCES, EISDIR, EIO) propagates instead of being mistaken for a first run.
Point it at a mounted volume, not a container’s ephemeral filesystem. A redeploy destroys the ephemeral one, and losing the store after the identity is published cannot be undone from the bot side.

The environment-variable store

For hosts with no writable disk. The value is read from an environment variable, and every write hands you a base64 blob to persist wherever your platform keeps secrets.
string
default:"CLOAK_KEYSTORE"
The environment variable to read and write. The default is exported as DEFAULT_KEYSTORE_ENV.
(blob: string) => void | Promise<void>
Called with the base64 blob after every write. This is what makes the environment store durable. Without it the identity dies with the process.
Reads accept either raw keystore JSON (so you can paste a file’s contents straight into a config field) or base64 of it. Writes always emit single-line base64, which is safe to paste anywhere.
Capture the blob via onWrite, or the bot bricks on restart. process.env does not survive the process. If the bot generates a fresh identity with no onWrite hook and no CLOAK_KEYSTORE already set, that identity is gone the moment the process exits, and because publication is write-once the server rejects the next run’s replacement forever. The SDK prints one loud warning the first time this happens, and it is not gated behind debug. Do not run past it.
A first deploy on this backend therefore has two steps: run once so the identity is generated and onWrite captures the blob, confirm the blob landed in your secret store, then restart and confirm the bot reuses it rather than warning again.

Writing your own adapter

An adapter is three methods. describe() is optional and only supplies a label for operator-facing error messages.

The one rule

read() resolving null is the only way to say “nothing stored yet”. Every other failure, a permissions error, a truncated read, a dead network, a timeout, an HTTP 500, must reject.
A failure that masquerades as a first run makes the SDK generate a brand new identity. Publication is write-once server-side, so that identity can never be published, and the bot is permanently broken through no fault of its own. Rejecting is always the safe answer: a bot that will not start is recoverable, a bot that regenerated its identity is not. Concretely, if you are wrapping a store that has a “not found” concept:
  • “not found” (a 404, ENOENT, a missing row) maps to null.
  • Everything else throws. Including “I could not tell”.

An HTTP secret-store adapter

This one talks to a secret service over fetch, so it needs no extra packages. Note how narrowly null is produced.
secretKeystore.ts
bot.ts

Things to get right

A reader must never observe half a keystore. A store with a single-key put gives you this for free. If yours does not, write to a staging key and swap.
read() is called once, before any network traffic and before any identity decision. Caching buys nothing and risks answering from a stale copy after an operator restores a backup.
Throw. The SDK decides what a failed save means, and it handles that decision in one place. An adapter that silently drops a write turns a loud problem into a quiet one.
The keystore holds live conversation keys. Anyone who can read it can decrypt your bot’s traffic. Use a secret store, not a config map, and keep it out of logs.
It is the label an operator sees when the SDK reports a keystore problem. secret store https://vault.internal/... is worth more at 3am than the default.

Backup and restore

Back the keystore up the way you would back up a private key, because it contains one.
  • Back it up after the first successful login, once the identity is published. That is the copy that matters.
  • Restore is a straight overwrite: put the bytes back where the adapter reads them.
  • The environment store’s blob is base64 of the same JSON, so a file backup and an environment backup are interconvertible.
Never clear a corrupt keystore. If the store exists but is corrupt or unreadable, login() throws before connecting rather than starting with a fresh identity. That is deliberate. Restore from a backup instead. Clearing it regenerates the identity, and because publication is write-once, that is a one-way trip: the only recovery left is resetting the bot’s identity server-side.

Next

Keys and the keystore

What the keys are, and why publication is write-once.

Deploying a bot

Where the keystore fits in a real deployment.

When a bot will not start

Symptom-indexed triage, including keystore failures.

Known limitations

The honest list of what does not work yet.