Degoog — Search engines

Custom search backends (web, images, video).

Where engines live

Custom engines live in data/engines/ (or DEGOOG_ENGINES_DIR). Each engine is a file or folder with an entry file that exports an object or class with name and executeSearch.

Engine contract

Required:

Optional:

SettingField shape: key, label, type (text, password, url, toggle, textarea, select, urllist), and optional required, placeholder, description, secret, options (for select), default.

Mark sensitive fields (API keys, tokens) with secret: true. The UI will never expose the saved value — it shows a Set / Not set indicator instead. On save, if the field value equals the sentinel "__SET__" the existing stored value is preserved, so a page reload never overwrites a key the user already entered.

HTTP method

Engines are not limited to GET requests. Since you control the fetch call inside executeSearch, you can use any method (POST, etc.) and set any headers or body your API requires. A GraphQL engine using POST looks like:

async executeSearch(query, page = 1, _timeFilter, context) {
  const doFetch = context?.fetch ?? fetch;
  const response = await doFetch("https://graphql.example.com", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: MY_QUERY, variables: { search: query, page } }),
  });
  const data = await response.json();
  // parse and return array of { title, url, snippet, source }
}

Search types

Export a named type: "web" (default), "images", "videos", "news", or a custom string (e.g. "books"). A custom type creates a new tab on the search results page; when selected, all engines with that type run.

Language & time filters

When a user picks a language or time filter in the search UI, those values are passed to every engine via the context argument. Reading them is optional — engines that ignore them simply return unfiltered results.

Field Type Description
context.lang string | undefined ISO 639-1 language code chosen by the user (e.g. "en", "de", "it"). undefined means no language filter was selected.
context.buildAcceptLanguage() () => string Returns a ready-to-use Accept-Language header value derived from context.lang (e.g. "it,en;q=0.9"). Falls back to "en,en-US;q=0.9" when no lang is set. Pass it directly to your fetch headers.
context.dateFrom string | undefined Start of a custom date range in YYYY-MM-DD format. Only set when timeFilter === "custom".
context.dateTo string | undefined End of a custom date range in YYYY-MM-DD format. Only set when timeFilter === "custom".

The timeFilter parameter passed to executeSearch is one of: "any", "hour", "day", "week", "month", "year", "custom". For "custom", read context.dateFrom / context.dateTo.

export default class MyEngine {
  name = "My Search";

  async executeSearch(query, page = 1, timeFilter, context) {
    const lang = context?.lang;
    const acceptLang = context?.buildAcceptLanguage?.() ?? "en,en-US;q=0.9";
    const doFetch = context?.fetch ?? fetch;

    const params = new URLSearchParams({ q: query });

    if (lang) params.set("lang", lang);

    if (timeFilter === "day") params.set("when", "24h");
    else if (timeFilter === "week") params.set("when", "7d");
    else if (timeFilter === "custom" && context?.dateFrom) {
      params.set("from", context.dateFrom);
      if (context.dateTo) params.set("to", context.dateTo);
    }

    const res = await doFetch(`https://example.com/search?${params}`, {
      headers: { "Accept-Language": acceptLang },
    });
    // parse and return array of { title, url, snippet, source }
  }
}

Safe Search

Built-in engines (Brave, Brave News, Google, Bing, DuckDuckGo) expose a Safe Search dropdown in Settings → Engines. The default keeps existing behaviour — Brave/Brave News default to moderate, the rest default to off. Users can raise or lower the filter per engine.

Custom engines can add the same setting by including a select field in settingsSchema and applying the value in executeSearch:

settingsSchema = [
  {
    key: "safeSearch",
    label: "Safe Search",
    type: "select",
    options: ["off", "moderate", "strict"],
    description: "Filter explicit content from search results.",
  },
];

safeSearch = "moderate";

configure(settings) {
  if (typeof settings.safeSearch === "string") {
    this.safeSearch = settings.safeSearch;
  }
}

async executeSearch(query, page = 1, timeFilter, context) {
  const params = new URLSearchParams({ q: query, safesearch: this.safeSearch });
  // ...
}

Proxies

When users enable a proxy for search, engine requests can go through it. For your engine to work with the proxy:

  1. Use context.fetch for every outbound HTTP request (not global fetch).
  2. Export outgoingHosts — array of hostname strings (no protocol or path). Those hostnames are added to the proxy allowlist. Use ["*"] only if the engine fetches from arbitrary user-configured URLs.
export const outgoingHosts = ["www.example.com", "example.com"];

export default class MyEngine {
  name = "My Search";
  async executeSearch(query, page = 1, _timeFilter, context) {
    const url = `https://www.example.com/search?q=${encodeURIComponent(query)}`;
    const doFetch = context?.fetch ?? fetch;
    const response = await doFetch(url, { headers: { "User-Agent": "my-engine/1.0" } });
    const html = await response.text();
    // parse and return array of { title, url, snippet, source, ... }
  }
}

Every engine also has an Outgoing HTTP client control under Advanced in Settings → Engines (Configure). It is stored per engine: fetch (default), system curl, auto (retry with curl after certain error statuses), or any custom transport installed via the Store. Custom engines should still use context.fetch so that setting is applied automatically.

Setup

Create data/engines/ (or set DEGOOG_ENGINES_DIR). Add a single file (e.g. my-engine.js) or a folder with index.js (or .ts, .mjs, .cjs). The engine id is the filename or folder name with an engine- prefix (e.g. ecosia/index.jsengine-ecosia).

How settings work

  1. Declare settingsSchema — Configure appears in Settings → Engines.
  2. User saves; values stored in data/plugin-settings.json.
  3. configure(settings) is called after save and on restart if settings exist.
  4. Return an empty array from executeSearch when required settings are missing.

Examples from the official store

When distributing via the Store, add a screenshots/ folder in the engine folder for the Store card.