Show HN: Cse-bridge, a drop-in for Google's Custom Search API (off Jan 2027) A developer released cse-bridge, a self-hosted HTTP service that emulates Google's customsearch/v1 wire format on top of a user's own SearXNG instance, ahead of Google shutting off the Custom Search JSON API on 2027-01-01. Google's suggested replacement, Vertex AI Search, requires rewriting code for a different API and response shape, while cse-bridge keeps existing client libraries, parsing code, pagination loops and cx values intact by changing only the base URL. The project installs via git clone and docker compose up -d, requires no API keys or per-query fees, and is also published as a prebuilt image at ghcr.io/booyaka101/cse-bridge. Google is shutting off the Custom Search JSON API on 2027-01-01. This keeps your code running by changing one base URL. "The Custom Search JSON API is closed to new customers. Existing Custom Search JSON API customers have until January 1, 2027 to transition to an alternative solution." — Google, Custom Search JSON API overview https://developers.google.com/custom-search/v1/overview Google's suggested replacement is Vertex AI Search — a different API, a different response shape, and a paid product. Every line you wrote against customsearch/v1 has to be rewritten. cse-bridge is the other option: a small self-hosted HTTP service that speaks Google's customsearch/v1 wire format on top of your own SearXNG https://github.com/searxng/searxng instance. Your client library, your parsing code, your pagination loop and your cx values all stay exactly as they are. - customsearch {version: 'v1'} + customsearch {version: 'v1', rootUrl: 'http://localhost:8080/'} No API keys. No per-query fees. No account. It is your machine talking to your SearXNG. git clone https://github.com/Booyaka101/cse-bridge.git cd cse-bridge docker compose up -d That is the whole install. Now make the call you were already making: A prebuilt image is also on GHCR — ghcr.io/booyaka101/cse-bridge — which the compose file uses automatically once pulled; docker compose up -d builds locally either way. curl 'http://localhost:8080/customsearch/v1?key=k&cx=default&q=rust%20async%20runtime&num=3' { "kind": "customsearch search", "url": { "type": "application/json", "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&cx={cx?}" }, "queries": { "request": { "title": "Google Custom Search - rust async runtime", "totalResults": "6", "searchTerms": "rust async runtime", "count": 3, "startIndex": 1, "inputEncoding": "utf8", "outputEncoding": "utf8", "safe": "off", "cx": "default" } , "nextPage": { "startIndex": 4, "count": 3, "...": "..." } }, "searchInformation": { "searchTime": 1.409902324, "formattedSearchTime": "1.41", "totalResults": "6", "formattedTotalResults": "6" }, "items": { "kind": "customsearch result", "title": "The Async Ecosystem - Asynchronous Programming in Rust", "htmlTitle": "The Async Ecosystem - Asynchronous Programming in Rust", "link": "https://rust-lang.github.io/async-book/08 ecosystem/00 chapter.html", "displayLink": "rust-lang.github.io", "snippet": "The Async Ecosystem Rust currently provides only the bare essentials for writing async code. Importantly, executors, tasks, reactors, combinators, and low-level I/O futures and traits are not yet provided in the standard library. ...", "htmlSnippet": "The Async Ecosystem Rust currently provides only the bare essentials for writing async code. ...", "formattedUrl": "https://rust-lang.github.io/async-book/08 ecosystem/00 chapter.html", "htmlFormattedUrl": "https://rust-lang.github.io/async-book/08 ecosystem/00 chapter.html" } } That is a real, unedited response from the stack above. Real results, from real engines, in Google's shape. Port 8080 already taken? Put CSE BRIDGE HOST PORT=8081 in a .env next to docker-compose.yml . These four are verified end to end against a live stack. Full recipes in docs/migrating-from-google-cse.md /Booyaka101/cse-bridge/blob/main/docs/migrating-from-google-cse.md . Node — @googleapis/customsearch js import { customsearch } from '@googleapis/customsearch'; const client = customsearch { version: 'v1', rootUrl: 'http://localhost:8080/' } ; const res = await client.cse.list { q: 'test', cx: 'default', auth: 'k' } ; console.log res.data.items.length ; // 10 Python — google-api-python-client python from google.api core.client options import ClientOptions from googleapiclient.discovery import build service = build "customsearch", "v1", developerKey="k", client options=ClientOptions api endpoint="http://localhost:8080" res = service.cse .list q="test", cx="default" .execute print len res "items" 10 LangChain — GoogleSearchAPIWrapper search = GoogleSearchAPIWrapper google api key="k", google cse id="default" search.search engine = build "customsearch", "v1", developerKey="k", client options=ClientOptions api endpoint="http://localhost:8080" search.run "test" curl / anything else — swap https://www.googleapis.com for http://localhost:8080 . You need a SearXNG instance with JSON output enabled see below . npm install -g cse-bridge SEARXNG URL=http://localhost:8888 cse-bridge cse-bridge 1.1.0 listening http://localhost:8080 endpoint http://localhost:8080/customsearch/v1 backend http://localhost:8888 profiles default, docs, news, code from profiles.yml auth disabled any key accepted Requires Node 22 or newer. The package has zero runtime dependencies . SearXNG does not serve JSON unless you turn it on. From the SearXNG search API docs https://docs.searxng.org/dev/search api.html : "Format needs to be activated in search: " . In settings.yml : search: formats: - html - json The bundled searxng/settings.yml already does this, so docker compose up just works. If you point at your own instance and forget, the bridge tells you exactly what to fix instead of failing mysteriously: {"error":{"code":503,"message":"The service is currently unavailable.","errors": {"message":"SearXNG at http://localhost:8888 returned HTML, not JSON. Enable it in settings.yml:\n search:\n formats:\n - html\n - json","domain":"global","reason":"backendError"} ,"status":"UNAVAILABLE"}} All configuration is environment variables. Every one has a working default. | Variable | Default | What it does | |---|---|---| | SEARXNG URL | http://localhost:8888 | Your SearXNG instance. Must have json in search.formats . | | PORT | 8080 | Listen port. | | HOST | 0.0.0.0 | Bind address. | | CSE BRIDGE KEYS | unset | Comma-separated accepted key values. Unset means the key param is not checked at all. | | PROFILES FILE | profiles.yml | cx → backend profile map. A missing file is fine. | | CSE BRIDGE TIMEOUT MS | 20000 | Per-request backend timeout. | | CSE BRIDGE CACHE TTL MS | 300000 | How long a query's result set stays stable. 0 disables caching — see Pagination pagination-and-why-there-is-a-cache . | | CSE BRIDGE CACHE MAX | 256 | Max distinct queries held in the cache. | | CSE BRIDGE PAGEMAP | off | on rebuilds item.pagemap by fetching the result pages. See Structured data structured-data-rebuilding-pagemap . A profile's pagemap: key overrides this. | | CSE BRIDGE PAGEMAP MAX | 10 | Result pages fetched per request; results sharing a page count once. 0 disables fetching. | | CSE BRIDGE PAGEMAP TIMEOUT MS | 3000 | Deadline for one page. | | CSE BRIDGE PAGEMAP BUDGET MS | 9000 | Deadline for the whole enrichment pass. Defaults to 3x the per-URL timeout. Items not reached in time come back bare. | | CSE BRIDGE PAGEMAP TTL MS | 3600000 | How long a fetched page stays cached. | | CSE BRIDGE PAGEMAP ALLOW PRIVATE | off | on lets pagemap fetch loopback and private addresses. Only needed for an intranet index. | Google's cx identified a Programmable Search Engine. Here it selects a block in profiles.yml , so a client you cannot edit keeps sending its existing cx and you decide server-side what it searches: default: description: General web search across the instance's enabled engines. categories: general docs: categories: general site: docs.rs every query on this cx gets an implicit site: filter news: categories: news pagemap: true rebuild item.pagemap for this cx only An unknown cx falls back to default — never an error, because a migrating client cannot change the cx it sends. | Endpoint | Purpose | |---|---| | GET /customsearch/v1 | The Google-shaped search endpoint. | | GET /healthz | Liveness plus backend reachability. | | GET /healthz?deep=1 | Also runs a real query, proving format=json is enabled. | /healthz deliberately does not search — a 30-second container healthcheck firing real queries would get your instance rate-limited by upstream engines. key , cx , q , num , start , hl , lr , safe , siteSearch , siteSearchFilter , dateRestrict , fileType , exactTerms , excludeTerms , sort , searchType , imgSize , imgType , imgColorType , imgDominantColor . A few behaviours are worth knowing: - num above 10 clamps to 10 instead of erroring. Google rejects it; clamping is friendlier and keeps start=1,11,21 loops walking. - start above 91 returns Google's exact error envelope , including status: "INVALID ARGUMENT" and errors 0 .reason: "badRequest" . - dateRestrict d7 , m6 , … maps onto SearXNG's coarser day / week / month / year buckets, always rounding up — you get a superset of what you asked for, never a subset. - siteSearch , fileType , exactTerms , excludeTerms become search operators in the backend query, since SearXNG has no dedicated parameters for them. - sort=date reorders by the publishedDate SearXNG attaches to news and paper results; undated results keep their relevance order and sit last. - searchType=image switches to SearXNG's images category — see Image search image-search below. image is the only accepted value, exactly as on Google. - imgSize , imgType , imgColorType , imgDominantColor are validated against Google's exact enums an out-of-enum value gets Google's 400, because Google rejects it too and then accepted for compatibility — SearXNG has no size/type/color parameters to map them onto, so they do not filter anything. Same posture as sort expressions beyond date . searchType=image works with the same one-line base-URL change as everything else. The link of each item is the image file itself what Google promises — clients hotlink it into