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:

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 the credential table.
  • Transparent Decryption: The Credential model automatically decrypts records on retrieval via decrypt_value(), wrapping secrets in SecretStr for 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.ts interact with encrypted metadata only, keeping plaintext keys server-side.
  • Resilient Error Handling: Failed decryptions surface as decryption_error flags 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:

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 →