Credential Management Flow for API Keys in Open Notebook
Open Notebook stores AI provider API keys as encrypted records in SurrealDB, automatically encrypting them on save via encrypt_value() in open_notebook/utils/encryption.py and decrypting them on retrieval through Pydantic models using SecretStr, exposing a clean to_esperanto_config() method for secure provider integration.
Open Notebook handles sensitive AI provider credentials through a hardened pipeline defined in open_notebook/domain/credential.py and orchestrated by the FastAPI layer in api/routers/credentials.py. This system ensures that API keys remain encrypted at rest while remaining instantly available for LLM inference through the Esperanto abstraction layer.
Architecture Overview
The credential management flow spans four distinct layers:
- Domain Model –
open_notebook/domain/credential.pydefines theCredentialPydantic model withSecretStrfields and encryption hooks - Encryption Service –
open_notebook/utils/encryption.pyprovidesencrypt_value()anddecrypt_value()for at-rest protection - API Layer –
api/routers/credentials.pyandapi/credentials_service.pyhandle HTTP transport and business logic validation - Frontend Client –
frontend/src/lib/api/credentials.tsandfrontend/src/lib/hooks/use-credentials.tsmanage UI state and server communication
The Credential Lifecycle
Creation and Encryption
When a user submits a new API key through the frontend, the payload reaches api/credentials_service.py and instantiates the Credential model. In open_notebook/domain/credential.py, the model validates the input using Pydantic and prepares the record for storage.
The api_key field is defined as a SecretStr type to prevent accidental logging. During the save() operation, the model calls _prepare_save_data(), which invokes encrypt_value() from open_notebook/utils/encryption.py to transform the plaintext key into ciphertext before it reaches the database.
from open_notebook.domain.credential import Credential
from pydantic import SecretStr
cred = Credential(
name="Production OpenAI Key",
provider="openai",
modalities=["language", "embedding"],
api_key=SecretStr("sk-xxxxxxxxxxxxxxxx"),
base_url=None,
endpoint=None,
)
await cred.save() # Encrypts api_key before DB write via SurrealDB
Database Persistence
The encrypted credential is persisted to the credential table in SurrealDB. The save() method from the base ObjectModel class handles the INSERT or UPDATE operation, storing the ciphertext in the api_key column while maintaining provider-specific options (such as num_ctx) in a flexible config JSON field. At no point does the database hold the unencrypted secret.
Retrieval and Decryption
When the system needs to access a credential, Credential.get(id) or Credential.get_all() overrides the base accessors to implement transparent decryption. After fetching the row from SurrealDB, the model applies decrypt_value() to the ciphertext and re-wraps the result in a SecretStr, ensuring downstream code receives a clear-text secret while the database layer only ever stored encrypted data.
cred = await Credential.get("<credential-id>")
# cred.api_key is a SecretStr containing the decrypted plaintext
config = cred.to_esperanto_config()
# Returns: {"api_key": "sk-xxxxxxxxxxxxxxxx", "endpoint": "...", "num_ctx": 8192}
Integration with AI Providers
The Credential.to_esperanto_config() method bridges the gap between secure storage and provider instantiation. This method constructs a dictionary matching the config argument expected by Esperanto's AIFactory.create_*() calls. It copies the decrypted api_key along with other non-null fields—such as base_url, endpoint, and project—into a plain dictionary consumed by the AI provider factory.
This design guarantees that the provider receives the correct secret without forcing callers to handle encryption logic or risk exposing keys in stack traces.
Frontend API Layer
The React frontend interacts with this flow through typed API clients. The use-credentials.ts hook leverages TanStack Query to cache credential data, while credentials.ts handles the HTTP transport.
// src/lib/api/credentials.ts
export const getCredentials = async () => {
const res = await fetch(`${API_BASE}/credentials`);
return res.json(); // Returns list of credential objects (keys remain server-side)
};
// src/lib/hooks/use-credentials.ts
import { useQuery } from '@tanstack/react-query';
import { getCredentials } from '@/lib/api/credentials';
export const useCredentials = () =>
useQuery(['credentials'], getCredentials);
Note that the frontend never handles unencrypted keys directly; the API returns credential metadata and references, while sensitive operations involving the decrypted key occur server-side.
Error Handling and Model Linking
If decryption fails—for example, when the server-side encryption key rotates—the model catches the exception and populates a decryption_error field. The api_key attribute is replaced with a placeholder SecretStr("UNDECRYPTABLE"), allowing the UI to surface the error to the user and prompt for credential re-entry without crashing the provider lookup.
Additionally, Credential.get_linked_models() queries the model table for rows whose credential field references the credential's ID. This enables the system to determine exactly which LLM and embedding models are authorized by a given key, facilitating dependency tracking and rotation workflows.
Summary
- Encryption at Rest: All API keys are encrypted via
encrypt_value()before hitting SurrealDB, ensuring ciphertext storage in thecredentialtable. - Transparent Decryption: The
Credentialmodel automatically decrypts records on retrieval viadecrypt_value(), wrapping secrets inSecretStrfor safe memory handling. - Provider Abstraction: The
to_esperanto_config()method exposes decrypted credentials to Esperanto factories without requiring manual decryption by callers. - Frontend Isolation: React hooks in
frontend/src/lib/hooks/use-credentials.tsinteract with encrypted metadata only, keeping plaintext keys server-side. - Resilient Error Handling: Failed decryptions surface as
decryption_errorflags with placeholder values, preventing runtime crashes during key rotation.
Frequently Asked Questions
How does Open Notebook prevent API keys from appearing in logs?
The Credential model uses Pydantic's SecretStr type for the api_key field, which masks the value as *********** when serialized or logged. Only explicit access via .get_secret_value() (or the internal decryption flow) exposes the plaintext, and this occurs only within the server-side to_esperanto_config() method before being passed to the AI provider.
What happens if the encryption key changes or is lost?
If decrypt_value() fails due to a key mismatch or corruption, the model catches the exception and sets the api_key field to a SecretStr("UNDECRYPTABLE") placeholder. The decryption_error attribute is populated with the exception details, allowing the application to display a specific error message and prompt the user to re-enter the credential through the UI.
Can credentials be shared across multiple AI models?
Yes. The Credential.get_linked_models() method queries the model table to return all LLM and embedding configurations that reference a specific credential ID. This one-to-many relationship allows a single API key to authorize multiple model instances, and the system tracks these dependencies to prevent accidental deletion of credentials still in use.
Where is the encryption logic implemented?
The encryption utilities reside in open_notebook/utils/encryption.py, providing the encrypt_value() and decrypt_value() functions used by the domain model. The Credential class in open_notebook/domain/credential.py orchestrates these calls during the _prepare_save_data() and post-load hydration phases, ensuring encryption is transparent to the rest of the application.
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 →