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:
- name — Display name in Settings → Engines.
-
executeSearch(query, page?, timeFilter?, context)
(async) — Return a Promise of an array of results. Each result is an
object with
title,url,snippet,source, and optionalthumbnail,duration. Thecontextargument providescontext.fetch; use it for all outbound HTTP requests so the engine respects the user’s proxy (Settings → General).
Optional:
-
bangShortcut — Enables
!shortcut queryto search this engine only (e.g.bangShortcut: "ecosia"→!ecosia linux). -
settingsSchema,
configure(settings) — Same as plugins. Configure
button in Settings → Engines; values in
data/plugin-settings.json.
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:
-
Use
context.fetchfor every outbound HTTP request (not globalfetch). -
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.js →
engine-ecosia).
How settings work
-
Declare
settingsSchema— Configure appears in Settings → Engines. -
User saves; values stored in
data/plugin-settings.json. -
configure(settings)is called after save and on restart if settings exist. -
Return an empty array from
executeSearchwhen required settings are missing.
Examples from the official store
-
Ecosia
— web engine with
outgoingHosts,bangShortcut, usescontext.fetch. - Startpage — web engine with optional Anonymous View settings.
- Internet Archive — file-type engine; can be used as a dependency by the File tab plugin.
When distributing via the Store, add a
screenshots/ folder in the engine folder for the Store
card.