SWIRL for Backstage

Troubleshooting

Start with the health endpoint. It needs no token and answers HTTP 200 only when Redis, the Celery search worker, and the Tantivy reader are all up.

curl -s http://localhost:8000/swirl/sapi/health/backstage/

The index is empty

Symptom: Backstage search returns nothing for terms you know are in the catalog, and GET /swirl/index/ returns an empty types list.

  • No collator run has finished yet. The catalog and TechDocs collators run on a schedule. Restart the Backstage backend to trigger a run, then watch its log for the indexing lines.
  • The run produced zero documents. A run that indexes nothing is aborted on purpose, so an empty collator cannot wipe a working index. The backend log carries a warning. Fix the collator, not SWIRL.
  • The engine module is not registered. Without a search.swirl block, the module logs a warning and steps aside. Check the block is present and that the backend can read it.
  • Another engine is still registered. Remove the Postgres, Elasticsearch, or Lunr engine module from packages/backend/src/index.ts. Backstage accepts one search engine.
  • Ingest is failing. Look for 401 or 403 from SWIRL in the Backstage backend log, then read the two sections below.

Search returns an error page for a term with no matches

Symptom: a term that matches something works, and a term that matches nothing returns HTTP 500 with MissingIndexError rather than an empty result list. The message names a document type, usually techdocs.

{"error": {"name": "MissingIndexError",
  "message": "SWIRL has no live index for the requested document type(s): techdocs."}}

Cause: a document type is registered but has no live index. The usual case is techdocs on an app with no TechDocs content. The collator collates zero documents and aborts its run, which is correct, so that type never goes live. With permissions on, Backstage sends the full list of registered types on every query, so the empty type is always in the list. A query with at least one hit returns 200 and a soft message. A query with no hits turned into a 500.

  • Fix. Upgrade both halves to 0.1.1: the npm package @swirl-search/backstage-plugin-search-backend-module-swirl@0.1.1 and the image swirlai/swirl-backstage:0.1.1. An empty result set is an empty result set there, and a type with no live index is reported as a soft message on both paths.
  • On 0.1.0, the workaround is to give the type an index or to stop registering it: build TechDocs so the collator has something to index, or remove the TechDocs collator from packages/backend/src/index.ts. Leave search.swirl.federated.enabled as it is. Turning the federated lane off does not help, because the term matches nothing in either lane.

yarn add fails with YN0016 quarantined

Symptom: installing the package on a fresh create-app stops before it starts.

YN0027: @swirl-search/backstage-plugin-search-backend-module-swirl@unknown can't be resolved to a satisfying range
YN0016: The version for tag "latest" is quarantined, and no lower version is available

Pinning the version gives YN0016: All versions satisfying "0.1.1" are quarantined. This is not a broken package. @backstage/create-app ships npmMinimalAgeGate: 3d in .yarnrc.yml, so any package published in the last 72 hours is refused unless its scope is preapproved. Add the scope, as install step 2 shows, and run the same command again.

HTTP 401 from SWIRL

SWIRL could not verify the Backstage plugin token. Four causes account for nearly all of these.

Cause What to check
The JWKS URL is wrong It must be the full URL, ending in /api/search/.backstage/auth/v1/jwks.json. The plugin id in the path must match the audience.
The JWKS URL is unreachable from the container Run docker compose exec swirl curl -s "$SWIRL_BACKSTAGE_JWKS_URL". A URL that works on your laptop can still fail inside the container.
Audience mismatch SWIRL_BACKSTAGE_AUDIENCE must equal search.swirl.audience in app-config. Both default to search.
Clock skew The token carries iat and exp. A container clock more than a few seconds off makes every token invalid. Check the clock in the container, not on the host.

One more case looks like a 401 but is not. When SWIRL_BACKSTAGE_JWKS_URL or SWIRL_BACKSTAGE_AUDIENCE is empty, Backstage token verification is switched off. No bearer token is then accepted as a Backstage token.

HTTP 403 on ingest

The token verified, but the identity behind it may not write the index. The ingest endpoints require the swirl.change_searchprovider permission.

  • The indexer mints a service token, one with no obo claim, which maps to the SWIRL user backstage:service. That user gets the ingest permission when it is first provisioned.
  • A token carrying an obo claim maps to a normal user in the Backstage Users group, which can search but cannot ingest. A 403 on ingest usually means a user token reached an ingest endpoint.
  • If the backstage:service user was edited in the SWIRL admin, check it still holds Can change search provider on the SearchProvider model.

HTTP 409 on begin

A generation is already open for that document type. SWIRL allows one open generation per type, so two indexing runs cannot write the same index at once.

This normally resolves itself. A crashed or killed indexing run leaves its generation open. SWIRL treats an open generation as stale after SWIRL_TANTIVY_BEGIN_TTL seconds, 7,200 in the image. The next run then proceeds.

To clear it now, open Search index generations in the SWIRL admin console. Select the rows in state open and run the abort action. Aborting a generation deletes only that generation. The live index keeps serving throughout.

HTTP 402 with an upgrade message

This is a SWIRL Enterprise condition and cannot happen on Community. The Backstage SKU license permits use inside Backstage only. A search request that does not carry a verified Backstage plugin token is refused with HTTP 402 and a message naming the full edition.

  • Calling /swirl/search/ with an admin API token, from a script or from the browsable API, produces this. Route the call through Backstage, or move to a full SWIRL Enterprise license.
  • The SWIRL Galaxy UI shows the same 402 as a banner. That is expected under a Backstage-only license.
  • The admin console, the login page, and the health endpoint stay reachable, so you can still configure sources.

Federated results are slow or missing

The federated lane is bounded by a timeout. A source that misses it is dropped from that query, rather than delaying the whole result set.

  • search.swirl.federated.timeoutMs in app-config is sent with each query as backstage_timeout_ms. The default is 5,000.
  • SWIRL rounds it to whole seconds and clamps it between 1 and 180.
  • Without that parameter, SWIRL uses SWIRL_TIMEOUT, which defaults to 10 seconds.
  • search.swirl.queryTimeoutMs, default 8,000, is the HTTP timeout the Backstage side applies. Keep it above the federation timeout, or Backstage gives up before SWIRL answers.

Other reasons federated results do not appear:

  • The provider does not carry a tag the module fans out to. Add backstage to the provider's tags, or list its tag in federated.providerTags.
  • The provider is not active, or its credentials are wrong. Query SWIRL directly with ?qs=term&providers=backstage and read the messages array.
  • The federated lane is off, either through federated.enabled: false or because the query named only indexed document types.

Memory

The container uses about 2.5 GiB resident when idle and about 2.6 GiB with a small catalog indexed. The image is about 7 GB on disk. Most of that is the machine learning libraries SWIRL imports in three processes, not the index; Tantivy accounts for about 150 MB.

  • Give the container at least 3 GiB. The shipped Kubernetes manifest requests 768 MiB and limits at 2 GiB, which suits a small catalog. Raise the limit if the pod is killed for exceeding memory.
  • A pod killed during indexing shows as an aborted or stale generation. The live index is untouched, so search keeps working on the previous one.
  • SWIRL_TANTIVY_WRITER_HEAP_MB is 64 in the image. Raising it speeds up large indexing runs and raises peak memory during them.

A slimmer image profile is planned. The numbers above are what this build measures.

host.docker.internal is missing on Linux

Symptom: SWIRL logs that it cannot fetch the Backstage signing key, and every request returns 401. host.docker.internal is built in on Docker Desktop but does not exist on Linux unless it is mapped.

The shipped compose file includes the mapping. Keep it:

extra_hosts:
  - "host.docker.internal:host-gateway"

Two alternatives if that does not suit your setup:

  • Put Backstage and SWIRL on the same Docker network and use the service name, for example http://backstage:7007/....
  • Use the host address on the Docker bridge, usually 172.17.0.1.

Check it from inside the container rather than guessing:

docker compose exec swirl curl -sv "$SWIRL_BACKSTAGE_JWKS_URL"

Getting more detail

docker compose logs -f swirl
kubectl logs -f deployment/swirl-backstage

For anything else, see Troubleshooting or contact SWIRL support.