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
- Store secrets in the SurrealDB
credentialtable viaopen_notebook/domain/credential.pyinstead of.envfiles to enable runtime updates. - Encrypt all secrets using Fernet with the
ENCRYPTION_KEYenvironment variable handled byopen_notebook/utils/encryption.py. - Use the
ProviderConfigsingleton inopen_notebook/domain/provider_config.pyfor in-memory caching and fast lookups. - Expose CRUD operations through
api/routers/credentials.pyand business logic inapi/credentials_service.py. - Decrypt keys only during model initialization via
to_esperanto_config()and inject them usingprovision_langchain_modelinopen_notebook/podcasts/models.py. - Rotate credentials without restarts using PATCH requests; the cache refreshes automatically.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →