How to Securely Configure Multi-Provider AI Credentials in Open Notebook

Open Notebook stores API keys for OpenAI, Anthropic, Google, and Ollama in an encrypted SurrealDB credential table rather than in environment variables, enabling runtime rotation and per-model credential selection without service restarts.

The lfnovo/open-notebook project decouples AI provider secrets from application configuration by persisting them in a structured database. This approach allows you to securely configure multi-provider AI credentials in database records, supporting multiple keys per provider and hot-swapping them at runtime. Instead of restarting containers to update keys, you modify records through the REST API or web UI, with the application decrypting secrets only when initializing model calls via the to_esperanto_config() method.

Architecture Overview

The Credential Domain Model

In open_notebook/domain/credential.py, the Credential class defines the schema for secrets. It inherits from BaseRecord, which automatically assigns a SurrealDB record ID such as credential:123. The model stores the provider name (e.g., openai, anthropic), an encrypted secret string, optional JSON config metadata, and an is_default boolean flag. The __repr__ method deliberately excludes the secret field to prevent accidental leaks in logs.

Provider Configuration Singleton

The ProviderConfig class in open_notebook/domain/provider_config.py acts as an in-memory registry. It loads credential records from SurrealDB at startup and caches them in RAM. Key methods include get_default(provider), which returns the credential marked as default for a specific AI provider, and add_config(provider, credential), which registers a new key. This singleton pattern ensures that credential lookups do not require repeated database queries during inference.

Encryption and Security Model

Secrets are never stored in plain text. The open_notebook/utils/encryption.py module initializes a Fernet cipher using the ENCRYPTION_KEY environment variable. When a credential is created via api/credentials_service.py, the service encrypts the secret before the Credential model is inserted into the credential table. Only during model execution does the to_esperanto_config() method decrypt the secret, returning a configuration dictionary suitable for the Esperanto AI-provider wrapper.

All endpoints in api/routers/credentials.py are protected by the same password middleware used for the API, ensuring only authenticated users can manage secrets.

Managing Credentials via the API

Creating a credential:

curl -X POST http://localhost:5055/api/credentials \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "openai",
    "name": "production-key",
    "secret": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "config": {"org_id": "org-abc123"},
    "is_default": true
  }'

Rotating a credential:

curl -X PATCH http://localhost:5055/api/credentials/credential:42 \
  -H "Content-Type: application/json" \
  -d '{
    "secret": "sk-newkeyyyyyyyyyyyyyyyyyyyyyy",
    "is_default": true
  }'

Behind the scenes, api/credentials_service.py validates the payload, triggers encryption via open_notebook/utils/encryption.py, and updates the ProviderConfig cache so subsequent requests use the new key immediately.

Integrating Credentials with AI Models

When a podcast model (or any AI model) needs to instantiate a provider client, it resolves the associated credential. In open_notebook/podcasts/models.py, the code retrieves the credential object and calls to_esperanto_config() to generate a provider-ready dictionary:

from open_notebook.domain.provider_config import ProviderConfig
from open_notebook.ai.provision import provision_langchain_model

credential = ProviderConfig().get_default("openai")
if credential:
    esp_config = credential.to_esperanto_config()
    llm = provision_langchain_model(
        provider=credential.provider,
        model_name="gpt-4o",
        config=esp_config,
    )

This pattern ensures that model definitions remain free of hard-coded secrets, referencing only the credential record ID.

Default Fallbacks and Migration

For legacy models that lack an explicit credential reference, open_notebook/podcasts/migration.py provides a helper that auto-links the first available credential for the target provider. This deterministic fallback guarantees that existing deployments continue to function after the database schema migrates to the credential-based system.

Summary

Frequently Asked Questions

How is the encryption key managed?

The application reads the ENCRYPTION_KEY environment variable at startup. This key must be a valid Fernet key (32 bytes, base64-encoded). If the variable is missing, the encryption module in open_notebook/utils/encryption.py raises an error, preventing the application from starting with an insecure configuration.

Can I use multiple API keys for the same provider?

Yes. The credential table supports multiple records per provider. You can designate one as the default using the is_default flag, and specific models can reference alternative credentials by ID. The ProviderConfig class maintains these mappings in memory for performance.

What happens if I delete the default credential?

If the default credential is removed, ProviderConfig.get_default() returns None. Models that rely on the default will fail to initialize until a new default is set or the model is explicitly updated to reference a different credential ID. Always ensure at least one credential exists for providers in active use.

Are credentials exposed in the web UI or logs?

No. The Credential model's __repr__ excludes the secret field, and the API responses from api/routers/credentials.py omit the decrypted secret. The Settings page at /settings displays only the credential name, provider, and default status, never the raw API key.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →