Security & Billing
Secrets encryption
Sealed boxes — the gateway can encrypt, only the provisioner can decrypt.
Project secrets are encrypted with libsodium sealed boxes (via the pure-Rust
dryoc crate). The design splits the keypair across two services so the
component that accepts secrets can never read them.
The asymmetric split
public key secret key
│ │
you ──plaintext──▶ Gateway ──seal──▶ ciphertext ──▶ Provisioner ──unseal──▶ engine env
(encrypt-only) (project_secrets) (decrypt-at-spawn)- The gateway holds only the public key. It seals plaintext on write and
stores the ciphertext in
project_secrets. It is mathematically unable to decrypt — sealed boxes are anonymous public-key encryption. - The provisioner holds the secret key. At container spawn it unseals each secret and injects it as an environment variable into the project's engine.
- A short hash of the public key is stored as
envelope_key_idso key rotation is trackable.
What's never exposed
- Plaintext is never persisted — only ciphertext lands in the database.
- The API never returns plaintext —
GETon secrets lists names and timestamps only. The Console shows a new secret's value once, at creation, and never again. - Secrets aren't written to the engine's volume — they exist only as process environment variables inside the container.
Lifecycle implication
Because secrets are injected at spawn, adding or rotating a secret takes effect when the engine next starts — there's no live reload. The Connectors and Secrets guides call this out where it matters.
Local parity
Self-hosted engines read secrets from ~/.config/thinkingroot/secrets.toml
(mode 0600) instead of a vault, exposing the same ctx.env names — so
Root Functions behave identically in both
environments.