How the Hindsight Control Plane UI Integrates with Its Backend API: Architecture and Implementation
The Hindsight control plane UI integrates with its backend API through a centralized ControlPlaneClient class in 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. This singleton exposes typed methods for every API endpoint while delegating actual HTTP requests to a private fetchApi<T> helper:
// 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:
- Parses JSON error payloads when available, falling back to plain text
- Displays user-friendly toasts via sonner (
toast.warningfor 4xx errors,toast.errorfor 5xx/network failures) - 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) 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:
// 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) manages the list of memory banks and the currently selected bank. It synchronizes URL state with server data:
// 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), any route change triggers reactive updates without explicit prop drilling.
Component-Level API Integration
Bank Selection and Creation
The 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:
// 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():
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:
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
ControlPlaneClientclass insrc/lib/api.tscentralizes 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
fetchApiwrapper.
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, 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 for operations that don't require global state synchronization, such as immediate document ingestion after form submission.
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 →