Add a search provider
Implement the SearchClient interface and register a SearchProvider so Dhaga's enrichment and news-watchlist features run against your web search backend — Brave, SerpAPI, or a self-hosted SearXNG.
Web search powers enrichment and the opt-in news/job-change watchlist. It sits
behind the SearchClient gateway in packages/core/src/search/types.ts, which
mirrors the LLM gateway exactly. Firecrawl is the default; adding Brave,
SerpAPI, or a self-hosted SearXNG is one implementation plus a registration.
The contract
The search contract is deliberately tiny — normalize whatever your backend
returns into { title, url, snippet }:
// packages/core/src/search/types.ts
export interface SearchResult {
title: string;
url: string;
snippet: string;
}
export interface SearchOptions {
limit?: number;
}
export interface SearchClient {
search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
}
export interface SearchProvider {
id: string;
isConfigured(): boolean;
createClient(): SearchClient;
}Implement and register
import { registerSearchProvider, selectSearchProvider } from "@dhaga/core";
import type { SearchProvider } from "@dhaga/core";
export const searxng: SearchProvider = {
id: "searxng",
isConfigured: () => Boolean(process.env.SEARXNG_URL),
createClient: () => ({
async search(query, options) {
const res = await fetch(
`${process.env.SEARXNG_URL}/search?format=json&q=${encodeURIComponent(query)}`,
);
const body: { results: { title: string; url: string; content: string }[] } =
await res.json();
return body.results
.slice(0, options?.limit ?? 5)
.map((r) => ({ title: r.title, url: r.url, snippet: r.content }));
},
}),
};
// in apps/web/src/dhaga.providers.ts
registerSearchProvider(searxng);
selectSearchProvider("searxng"); // or leave it to SEARCH_PROVIDER=searxngSelection and graceful degradation
getSearchClient() resolves the active provider from selectSearchProvider(...),
then SEARCH_PROVIDER, then the firecrawl default
(packages/core/src/search/index.ts). Features that use search first call
hasSearch() — which returns your provider's isConfigured() — and degrade
quietly when no provider is configured, so an unconfigured search backend
never crashes a request; it just turns the feature off.
Return an empty array, not an error, for no results
search() should resolve to [] when a query has no hits. Reserve thrown
errors for genuine failures (auth, network) — callers treat an empty result as
"nothing found" and an exception as "the provider is broken".
Adding Brave or SerpAPI is the same shape — a different createClient() body
and a different isConfigured() env check. See
Providers for packaging and distribution.
Add an LLM provider
Implement the LLMClient interface and register an LLMProvider so Dhaga routes extraction and reasoning to your model — Ollama, a BYO-key gateway, or any OpenAI-compatible server.
Add a vector store / retrieval provider
Swap Dhaga's embedding model and vector store independently through the retrieval gateway — any external engine, as long as declared dimensions match.