Skip to main content
Curia keeps every credential — LLM API keys, Nylas, Signal, Tavily, the HTTP API token, the console login secret — in an encrypted vault rather than in plaintext .env files. The vault is the single source of truth for secrets; skills read from it through a scoped accessor, and the LLM never sees a value.

How it works

Secrets are encrypted with AES-256-GCM and stored in a secrets table in Postgres. Each row holds the ciphertext and its initialization vector, never the plaintext. The key that encrypts and decrypts them — SECRET_ENCRYPTION_KEY — is the one secret that has to live outside the vault, in .env. At runtime a skill calls ctx.secret("nylas_api_key"); the execution layer checks that the skill declared that key, decrypts the value, and hands it to the handler. Every access is audit-logged (which skill, which task, when, and whether the value came from the vault or an env fallback), but the value itself is never written to a log.

What stays in .env

Only the four values needed to reach and unlock the vault, plus non-secret deployment config:
VariablePurpose
DB_USERPostgreSQL user.
DB_PASSWORDPostgreSQL password.
DATABASE_URLPostgres connection string (where the vault lives).
SECRET_ENCRYPTION_KEYBase64-encoded 32-byte AES-256 master key that decrypts the vault.
pnpm run setup generates the database password and the master key, writes them to .env, and seeds the rest into the vault.
Back up SECRET_ENCRYPTION_KEY. It is the only thing that can decrypt the vault. If you lose it, every stored secret becomes permanently unrecoverable and you have to re-seed from scratch. Keep a copy somewhere independent of the server — a password manager or a secrets store.

Seeding secrets

To add or update a secret, set it transiently on the command line and run the seed script. The value is read from the process environment, encrypted, and stored under its canonical vault key:
ANTHROPIC_API_KEY=sk-ant-... pnpm run seed-vault
You can seed several at once:
NYLAS_API_KEY=nyk_v0_... \
NYLAS_GRANT_ID=<grant-id> \
[email protected] \
pnpm run seed-vault
After seeding, recreate the container so running skills pick up the change:
docker compose up -d --force-recreate curia

Canonical vault keys

The seed script maps each environment variable to a lowercase snake_case vault key:
Env var (transient)Vault key
ANTHROPIC_API_KEYanthropic_api_key
OPENAI_API_KEYopenai_api_key
OPENROUTER_API_KEYopenrouter_api_key
API_TOKENapi_token
WEB_APP_BOOTSTRAP_SECRETweb_app_bootstrap_secret
NYLAS_API_KEYnylas_api_key
NYLAS_GRANT_IDnylas_grant_id
NYLAS_SELF_EMAILnylas_self_email
SIGNAL_PHONE_NUMBERsignal_phone_number
TAVILY_API_KEYtavily_api_key
Channel credentials managed through the channel registry are stored under namespaced keys following the convention channel.<name>.<field> (e.g. channel.email.nylas_api_key).

Rotating the master key

Rotating SECRET_ENCRYPTION_KEY re-encrypts every stored secret under a new key. Supply both the old and new keys and the database connection:
SECRET_ENCRYPTION_KEY_OLD=<old-base64> \
SECRET_ENCRYPTION_KEY_NEW=<new-base64> \
DATABASE_URL=<url> \
pnpm exec tsx scripts/rotate-secret-key.ts
Generate a new key with openssl rand -base64 32. After rotation, replace SECRET_ENCRYPTION_KEY in .env with the new value and recreate the container. Keep the old key until you have confirmed the instance comes up cleanly with the new one.

Skills that require a secret

A skill can declare that a secret must be present before it can be used. Declaring a vault key in the skill manifest’s install.requires_secrets makes the registry refuse to install or enable the skill until that key is in the vault. The bundled web-search skill uses this for tavily_api_key — it stays disabled until you provision a Tavily key.

Managing skills, agents, and channels

Install and enable capabilities, and how required-secret gates work.

Configuration reference

Every configurable setting, plus the vault and key-rotation commands.