# How the Hindsight Control Plane UI Integrates with Its Backend API: Architecture and Implementation

> Discover how the Hindsight control plane UI integrates with its backend API using ControlPlaneClient, React Context, and declarative data fetching for efficient state management.

- Repository: [vectorize-io/hindsight](https://github.com/vectorize-io/hindsight)
- Tags: architecture
- Published: 2026-03-13

---

**The Hindsight control plane UI integrates with its backend API through a centralized `ControlPlaneClient` class in [`src/lib/api.ts`](https://github.com/vectorize-io/hindsight/blob/main/src/lib/api.ts) that wraps all HTTP requests, while React Context providers (`FeaturesProvider` and `BankProvider`) manage server state and feature flags, enabling components to consume API data declaratively without direct fetch logic.**

The vectorize-io/hindsight repository implements a modern React-based control plane that orchestrates memory banks and document ingestion. Rather than scattering fetch calls across components, the architecture centralizes all API communication through a purpose-built client layer, ensuring consistent error handling and type safety across the application.

## Central API Client Architecture

### ControlPlaneClient Implementation

All backend communication flows through the **`ControlPlaneClient`** class defined in [`src/lib/api.ts`](https://github.com/vectorize-io/hindsight/blob/main/src/lib/api.ts). This singleton exposes typed methods for every API endpoint while delegating actual HTTP requests to a private `fetchApi<T>` helper:

```typescript
// src/lib/api.ts
export class ControlPlaneClient {
  private async fetchApi<T>(path: string, options?: RequestInit): Promise<T> {
    const response = await fetch(path, {
      ...options,
      headers: { "Content-Type": "application/json", ...options?.headers },
    });
    if (!response.ok) { /* show toast & throw error */ }
    return response.json();
  }

  async listBanks() = this.fetchApi<{ banks: any[] }>("/api/banks");
  async createBank(bankId: string) = this.fetchApi<{ bank_id: string }>("/api/banks", { 
    method: "POST", 
    body: JSON.stringify({ bank_id: bankId }) 
  });
  async getVersion() = this.fetchApi<{ api_version: string; features: object }>("/api/version");
  async retain(params: object) = this.fetchApi("/api/memories/retain", { 
    method: "POST", 
    body: JSON.stringify(params) 
  });
}

```

Components import the singleton instance via `import { client } from "@/lib/api"`, ensuring a single source of truth for API configuration. The `uploadFiles` method handles multipart/form-data separately from the standard JSON pipeline used by `fetchApi`.

### Singleton Pattern and Error Handling

The exported `client` singleton standardizes error handling across the UI. When `fetchApi` encounters a non-OK response, it:

1. Parses JSON error payloads when available, falling back to plain text
2. Displays user-friendly toasts via **sonner** (`toast.warning` for 4xx errors, `toast.error` for 5xx/network failures)
3. Re-throws the error to allow calling components to react (e.g., keeping dialogs open on failure)

This pattern eliminates repetitive `try/catch` blocks in UI components while ensuring users receive immediate feedback on API failures.

## State Management Through React Context

The UI abstracts API data into React Context providers, allowing components to consume server state without direct client imports.

### FeaturesProvider and Feature Flags

The **`FeaturesProvider`** (located in [`src/lib/features-context.tsx`](https://github.com/vectorize-io/hindsight/blob/main/src/lib/features-context.tsx)) loads capability flags on application mount by calling `client.getVersion()`. It exposes flags like `observations`, `bank_config_api`, and **`file_upload_api`** through the `useFeatures()` hook:

```typescript
// src/lib/features-context.tsx (lines 33-47)
const features = await client.getVersion();
// features.features contains boolean flags from the server

```

This creates a UI-to-API contract where the backend dictates available functionality. For example, the file upload tab disables itself when `file_upload_api` returns false, preventing UI elements from appearing for unsupported endpoints.

### BankProvider for Memory Bank State

The **`BankProvider`** ([`src/lib/bank-context.tsx`](https://github.com/vectorize-io/hindsight/blob/main/src/lib/bank-context.tsx)) manages the list of memory banks and the currently selected bank. It synchronizes URL state with server data:

```typescript
// src/lib/bank-context.tsx (lines 21-32)
const banks = await client.listBanks();
// Parses usePathname to determine currentBank from URL

```

Components access this state through `useBank()`, which provides `currentBank`, `banks`, and the `loadBanks()` refresh function. Because this provider wraps the application root (in [`src/app/layout.tsx`](https://github.com/vectorize-io/hindsight/blob/main/src/app/layout.tsx)), any route change triggers reactive updates without explicit prop drilling.

## Component-Level API Integration

### Bank Selection and Creation

The [`src/components/bank-selector.tsx`](https://github.com/vectorize-io/hindsight/blob/main/src/components/bank-selector.tsx) component demonstrates direct client usage combined with React context. When creating a new bank, it chains API calls with router navigation:

```tsx
// src/components/bank-selector.tsx
await client.createBank(newBankId.trim());
await loadBanks(); // Refreshes context via client.listBanks()
router.push(`/banks/${newBankId.trim()}?view=data`);

```

The selector uses `useBank()` to read the current bank and `useFeatures()` to conditionally render the "Create Bank" button based on the `bank_config_api` flag.

### Document Ingestion and File Uploads

Document creation follows two API paths depending on input type. For text-only documents, components call `client.retain()`:

```tsx
await client.retain({
  bank_id: currentBank,
  items: [{ content: documentText, tags: selectedTags }],
  async: false, // Synchronous processing
});

```

For file uploads, the UI checks the `file_upload_api` feature flag before enabling the upload tab, then calls:

```tsx
await client.uploadFiles({
  bank_id: currentBank,
  files: selectedFiles,
  async: true,
  files_metadata: perFileMeta, // Array of tag/document_id metadata per file
});

```

Both paths maintain the same error handling flow through the central client, ensuring consistent toast notifications for ingestion failures.

## Routing and State Synchronization

The integration leverages Next.js routing to synchronize UI state with API resources. When users select a different bank via the dropdown, the component updates the router (`router.push`), which triggers a re-render of bank-dependent pages. Because `BankProvider` parses the URL pathname to determine `currentBank`, the API client never needs to manage routing logic directly—components simply call `useBank()` to receive the correct bank identifier for subsequent API calls.

## Summary

- The **`ControlPlaneClient`** class in [`src/lib/api.ts`](https://github.com/vectorize-io/hindsight/blob/main/src/lib/api.ts) centralizes all HTTP requests, providing typed methods for endpoints like `/api/banks`, `/api/version`, and `/api/memories/retain`.
- **React Context providers** (`FeaturesProvider`, `BankProvider`) consume the client to expose server state and feature flags, enabling declarative data access via hooks.
- **Error handling** occurs exclusively in `fetchApi`, which displays toast notifications through sonner while preserving error propagation for component-level handling.
- **Feature flags** from `client.getVersion()` gate UI functionality, ensuring the frontend only exposes capabilities supported by the current API version.
- **File uploads** use a specialized path in the client to handle multipart/form-data, while standard JSON requests flow through the centralized `fetchApi` wrapper.

## Frequently Asked Questions

### How does the Hindsight UI handle API authentication and headers?

The `ControlPlaneClient` automatically injects `"Content-Type": "application/json"` headers in the `fetchApi` method. According to the source code in [`src/lib/api.ts`](https://github.com/vectorize-io/hindsight/blob/main/src/lib/api.ts), the client spreads additional headers from the `options` parameter, allowing components to pass authentication tokens or custom headers when needed. All cookies and credentials follow standard browser fetch behavior for same-origin requests.

### What happens when the Hindsight API returns an error?

All API responses flow through the private `fetchApi` method, which checks `response.ok` before parsing JSON. When the server returns a 4xx error, the client displays a warning toast; 5xx errors trigger error toasts. The method attempts to parse JSON error bodies first, falling back to plain text if parsing fails, then re-throws the error so calling components can execute cleanup logic (such as keeping dialog boxes open).

### How does the UI know which features the backend supports?

On application mount, the `FeaturesProvider` calls `client.getVersion()` to retrieve capability flags including `file_upload_api`, `bank_config_api`, and `observations`. These boolean flags determine which UI tabs and buttons render. For example, the file upload interface only appears when `file_upload_api` is true, preventing users from attempting unsupported operations.

### Can components call the API directly without using the context providers?

Yes. While contexts like `BankProvider` offer convenience for shared state, any component can import the singleton client directly via `import { client } from "@/lib/api"` and call methods like `client.retain()` or `client.createBank()`. This pattern is used in [`src/components/bank-selector.tsx`](https://github.com/vectorize-io/hindsight/blob/main/src/components/bank-selector.tsx) for operations that don't require global state synchronization, such as immediate document ingestion after form submission.