Skip to main content
ProBot is bring-your-own-key. You supply an Anthropic / OpenAI / Azure / Google key; ProBot keeps it envelope-encrypted at rest, decrypts it in memory for one chat request, and discards it. The key is never logged, never echoed in an error message, and never returned in a response. A canary-key test enforces this at both the route layer and the provider adapter layer - if a regression ever leaks the key, the build fails.

Encryption at rest

On the hosted deployment, each key is protected by two layers:
  • A per-bot DEK (Data Encryption Key, AES-256-GCM) encrypts the key itself.
  • A KEK (Key Encryption Key) loaded from the PROBOT_KEY_ENCRYPTION_KEY env var wraps the DEK. The KEK never touches the database.
The wrapped DEK + ciphertext live in the encrypted_llm_keys table (one row per bot). A database-only leak is useless without the KEK. Rotating the KEK re-wraps every DEK without touching the ciphertext keys themselves - see KEY-STORAGE.md.

How a chat request resolves the key

The chat route accepts an optional x-llm-api-key header (used by the owner’s own dashboard test chat) and falls back to the managed encrypted key otherwise:
  1. Header present (owner testing from their own browser) - used directly.
  2. Managed encrypted key - unwrapped with the KEK, decrypted in memory, used once, discarded.
  3. Neither - request fails with missing_llm_key.
Recruiters chatting on the public URL or through the embed widget never have a header key - path 2 handles them. Every managed decrypt also writes a decrypt audit log row (timestamp + non-reversible hash) so anomalies are observable.

Where the key is

The JSON body is { message, sessionId } - the key is never a body field on any surface.

Why this shape

A traditional “AI chatbot service” stores your API key server-side in plaintext, calls the provider on your behalf, and gives you no visibility. That leaves a persistent, subpoenable secret sitting in a shared database. ProBot’s design inverts that:
  • Ciphertext at rest, plaintext for microseconds. A DB dump alone reveals nothing. Only a running server with the KEK can decrypt.
  • No key column in bots or users. The encrypted_llm_keys table is the only place a key ever appears, and only as ciphertext.
  • You pay the provider directly. Envelope encryption doesn’t change your billing relationship - ProBot never proxies charges.

Provider adapter - per-request client

Every adapter (Anthropic, OpenAI, Azure, Google, Grok) constructs a new client per request with the decrypted key. There is no let cachedClient, no Map<userId, Client>, no global. The canary-key test plants a known canary string as the plaintext key and asserts it appears only in the outbound HTTPS payload - never in a response, never in a log mock, never in an error.

Implementation footnotes

  • Azure keys are header-only. Azure’s multi-secret credential (key + endpoint + apiVersion) isn’t supported by managed storage in this release, so Azure bots require the owner to be online with the credentials in their browser store, or a self-hosted bot via the probot-self-hosted npm package. Other providers all go through managed encryption.
  • Self-hosting the bot. If you’d rather not trust any operator, install the probot-self-hosted npm package in your own web app - the plaintext key lives entirely inside your backend and never touches pro-bot.dev.
  • Owner test chat. When the owner tests their bot from the dashboard, the browser passes the key as x-llm-api-key. The dashboard mirrors the key into a browser-side encrypted store (IndexedDB + non-extractable Web Crypto key) so the owner doesn’t re-paste it on every reload.

Failure modes (and how the key is protected in each)

Threat model

BYO-key with envelope encryption protects you against:
  • Server-side breach of ProBot’s database. Ciphertext + wrapped DEK are useless without the KEK.
  • Cross-tenant leakage. Every request constructs a fresh provider client with the just-decrypted key.
  • Vendor lock-in / surprise billing. You hold the contract with Anthropic / OpenAI / Azure directly.
BYO-key does not protect you against:
  • Full infrastructure compromise. Anyone with deploy access can read the KEK. If that’s your threat model, self-host the bot with the probot-self-hosted npm package - your LLM key never touches pro-bot.dev.
  • XSS on the ProBot frontend. The frontend mitigates this with react-markdown + no rehype-raw; if you find a gap, please report via SECURITY.md.
  • Phishing. A lookalike domain could capture pasted keys.
For the input/output sanitization layer, see Security.