# Credential Management Flow for API Keys in Open Notebook

> Discover the credential management flow for API keys in Open Notebook. Learn how AI provider keys are securely encrypted in SurrealDB and decrypted using Pydantic models for safe integration.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-25

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) and orchestrated by the FastAPI layer in [`api/routers/credentials.py`](https://github.com/lfnovo/open-notebook/blob/main/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.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/credential.py) defines the `Credential` Pydantic model with `SecretStr` fields and encryption hooks
- **Encryption Service** – [`open_notebook/utils/encryption.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) provides `encrypt_value()` and `decrypt_value()` for at-rest protection
- **API Layer** – [`api/routers/credentials.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/credentials.py) and [`api/credentials_service.py`](https://github.com/lfnovo/open-notebook/blob/main/api/credentials_service.py) handle HTTP transport and business logic validation
- **Frontend Client** – [`frontend/src/lib/api/credentials.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/credentials.ts) and [`frontend/src/lib/hooks/use-credentials.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/hooks/use-credentials.ts) manage 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`](https://github.com/lfnovo/open-notebook/blob/main/api/credentials_service.py) and instantiates the `Credential` model. In [`open_notebook/domain/credential.py`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/encryption.py) to transform the plaintext key into ciphertext before it reaches the database.

```python
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.

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/use-credentials.ts) hook leverages TanStack Query to cache credential data, while [`credentials.ts`](https://github.com/lfnovo/open-notebook/blob/main/credentials.ts) handles the HTTP transport.

```typescript
// 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)
};

```

```typescript
// 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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.