Transports
Set per index. All three answer from the same artifact and the same two engines — what changes is where the query runs and what crosses the wire.
Fragment — the default
A refinement fetches /caffeine/fragment, which re-renders the same template with the same element in context and returns the regions that changed. Hit markup stays in Twig, so a refined page is byte-identical to a full load of the same URL — because it came from the same code.
The response carries every state-dependent region, not just the results. Swapping only the hits would leave the facet counts beside them describing the previous query — “Acme (12)” next to three results — which is the specific bug that makes hand-rolled filtering feel broken.
With HTMX
HTMX is supported but not required: the bundled runtime does its own fetch and swap in a few kilobytes. Passing { htmx: true } to the tag instead emits hx-boost on the wrapper and leaves the runtime off, for sites already running HTMX.
That path fetches the page and selects the results block out of it, so facet counts elsewhere on the page keep their previous numbers. Put the widgets inside {% caffeineresults %} if that matters, or use the bundled runtime, which swaps every region.
Client-side
The browser fetches the artifact once and answers every refinement locally — no request, no latency, no server load. Two requests on load, both cacheable: the pointer is small and revalidated, the payload it names is immutable and cached forever.
Hits render from a template on the page, because no request is made and Twig is not available:
<template data-caffeine-hit>
<li><a href="{{ url }}">{{ title }}</a></li>
</template>
Facet counts are patched in place on the server-rendered controls. That is sound rather than merely convenient: a cached page is rendered unrefined, and an unrefined result contains every value a facet has, so refining can only ever reduce a count — never introduce a value that was not already on the page. The exception is a facet truncated by the values-per-facet limit.
Comfortable to roughly 50,000 records. Past that the artifact is megabytes and the browser is doing too much work on load.
Algolia JSON
Exposes /caffeine/search/<handle> returning the Algolia search-response shape, so InstantSearch, React and Vue InstantSearch and Autocomplete.js work against Caffeine unmodified.
Only indexes set to this transport answer; the endpoint returns 403 otherwise, so an index is never published as JSON by accident. It accepts both a posted JSON body — what an InstantSearch adapter sends — and a plain query string, so it can be opened in a browser tab while debugging.
The wire format is the Algolia response shape throughout, which also means a site can graduate to Typesense, Meilisearch or Algolia later without rewriting its front end.
Choosing
| If… | Use |
|---|---|
| You want hit markup in Twig, and a site that works everywhere | Fragment |
| The index is small and you want zero latency | Client-side |
| You already have an InstantSearch front end | Algolia JSON |
| The index is over ~50,000 records | Fragment |
Lite uses the fragment transport. The other two need Pro.