> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloak.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Keystore backends

> Where your bot's keys live. The file store, the environment-variable store, and how to write an adapter for a host with no writable disk.

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](/concepts/keys-and-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

| Your host                                     | Use                        |
| --------------------------------------------- | -------------------------- |
| Has a durable disk or a mounted volume        | `keystorePath`             |
| Has a secret store but no writable disk       | `envKeystore({ onWrite })` |
| Has neither, but has a database, S3, or Vault | your own `KeystoreAdapter` |

`keystorePath` and `keystore` are mutually exclusive. Passing both throws.

## The file store

```ts theme={null}
import { Client } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystorePath: '/data/bot.cloak-keystore.json',
});
```

`keystorePath` is shorthand for `keystore: fileKeystore(path)`, and you can pass the adapter directly if you prefer to be explicit:

```ts theme={null}
import { Client, fileKeystore } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystore: fileKeystore('/data/bot.cloak-keystore.json'),
});
```

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.

<Warning>
  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.
</Warning>

## 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.

```ts theme={null}
import { Client, envKeystore } from '@cloak-software/bot-sdk';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystore: envKeystore({
    onWrite: async (blob) => {
      await myPlatform.setSecret('CLOAK_KEYSTORE', blob);
    },
  }),
});
```

<ParamField path="varName" type="string" default="CLOAK_KEYSTORE">
  The environment variable to read and write. The default is exported as `DEFAULT_KEYSTORE_ENV`.
</ParamField>

<ParamField path="onWrite" type="(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.
</ParamField>

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.

<Warning>
  **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.
</Warning>

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.

```ts theme={null}
export interface KeystoreAdapter {
  /** The stored keystore JSON text, or null when nothing is stored yet. */
  read(): Promise<string | null>;
  /** Persist the full keystore JSON text. Implementations should be atomic. */
  write(contents: string): Promise<void>;
  /** Human label used in operator-facing errors, e.g. `file ./bot.keystore.json`. */
  describe?(): string;
}
```

### The one rule

<Warning>
  `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**.
</Warning>

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.

```ts secretKeystore.ts theme={null}
import type { KeystoreAdapter } from '@cloak-software/bot-sdk';

export function secretKeystore(baseUrl: string, token: string): KeystoreAdapter {
  const url = `${baseUrl}/secrets/cloak-bot-keystore`;
  const auth = { authorization: `Bearer ${token}` };

  return {
    async read(): Promise<string | null> {
      const res = await fetch(url, { headers: auth });
      // The ONLY null. A 404 is genuinely "nothing stored yet".
      if (res.status === 404) return null;
      // Everything else rejects, including 5xx and a network throw, so the
      // SDK fails loudly instead of regenerating a write-once identity.
      if (!res.ok) {
        throw new Error(`keystore read failed: ${res.status} ${res.statusText}`);
      }
      return await res.text();
    },

    async write(contents: string): Promise<void> {
      const res = await fetch(url, {
        method: 'PUT',
        headers: { ...auth, 'content-type': 'application/json' },
        body: contents,
      });
      if (!res.ok) {
        throw new Error(`keystore write failed: ${res.status} ${res.statusText}`);
      }
    },

    describe(): string {
      return `secret store ${url}`;
    },
  };
}
```

```ts bot.ts theme={null}
import { Client } from '@cloak-software/bot-sdk';
import { secretKeystore } from './secretKeystore.js';

const client = new Client({
  token: process.env.CLOAK_TOKEN!,
  keystore: secretKeystore(process.env.SECRET_API!, process.env.SECRET_TOKEN!),
});
```

### Things to get right

<AccordionGroup>
  <Accordion title="Make writes atomic, or at least all-or-nothing">
    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.
  </Accordion>

  <Accordion title="Do not cache reads">
    `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.
  </Accordion>

  <Accordion title="Do not swallow write failures">
    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.
  </Accordion>

  <Accordion title="Treat the contents as a secret, not as config">
    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.
  </Accordion>

  <Accordion title="Return something useful from describe()">
    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.
  </Accordion>
</AccordionGroup>

## 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.

<Warning>
  **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.
</Warning>

## Next

<CardGroup cols={2}>
  <Card title="Keys and the keystore" icon="key" href="/concepts/keys-and-keystore">
    What the keys are, and why publication is write-once.
  </Card>

  <Card title="Deploying a bot" icon="rocket" href="/production/deploying">
    Where the keystore fits in a real deployment.
  </Card>

  <Card title="When a bot will not start" icon="stethoscope" href="/production/troubleshooting">
    Symptom-indexed triage, including keystore failures.
  </Card>

  <Card title="Known limitations" icon="triangle-exclamation" href="/production/known-limitations">
    The honest list of what does not work yet.
  </Card>
</CardGroup>
