Bring Your Own Stack
indx owns the orchestration — the six-stage pipeline — and hands you the components. Every expensive or opinionated capability (parsing, LLM, vision, embedding, vector store, output) is a typed slot with a sensible default that you can replace by name or by object. This is what we mean by Bring Your Own Stack (BYOS): your environment is the constraint that matters, so the tool bends to it rather than the other way around.
The idea in one sentence
Section titled “The idea in one sentence”indx composes parsers, models, and stores; it does not replace them. A file parser is not a competitor — it is one swappable slot inside indx. The same is true of the LLM that writes summaries, the embedder that vectorizes chunks, the database that stores them, and the writer that serializes the result.
Because each slot is a typed interface with a named default, swapping a backend is a config change, never a rewrite. The pipeline you write today survives the model you will use next year: re-embed, re-store, re-export — same code.
The six slots
Section titled “The six slots”The six stages of the pipeline read and write capabilities through six replaceable slots. Each slot has exactly one default — the shipped, zero-config stack is cloud-backed (openai:gpt-5-mini for the LLM, openai:text-embedding-3-small for embeddings). For an offline, air-gapped run, the zero-dependency core fallbacks and the opt-in local profile (ollama:qwen2.5 + bge-m3) swap those slots out so a complete run works with no network and no API key.
| Slot | Default | Other options |
|---|---|---|
| Parser | docling | unstructured, llamaparse, markitdown, custom |
| LLM | openai:gpt-5-mini (cloud) | ollama, vllm, anthropic, azure |
| VLM | none | qwen-vl, gpt-4o, local |
| Embedding | openai:text-embedding-3-small (dim 1536) | bge-m3, e5, cohere |
| Vector store | qdrant | pgvector, chroma, lancedb, jsonl |
| Output | .indx | jsonl, langchain, llamaindex |
Each slot has a dedicated guide that covers the trade-offs in depth:
Three principles BYOS serves
Section titled “Three principles BYOS serves”The slot design is not decoration — it is the structural expression of three product principles that recur across indx.
1. Local-first defaults
Section titled “1. Local-first defaults”The local profile runs on a developer’s laptop with no external services. docling parses locally, ollama:qwen2.5 runs enrichment with no API key, bge-m3 embeds locally, and the store and output writers can both stay entirely offline. There is also a zero-dependency floor that ships in the core package itself — the plaintext parser, the jsonl store, the none VLM, and the .indx + jsonl writers — so even before you install a single extra, a full run is possible offline.
2. No lock-in
Section titled “2. No lock-in”Slots are defined as typing.Protocol classes — structural typing. A backend satisfies a slot by having the right methods and signatures, not by inheriting from an indx base class. Third-party authors never import or subclass anything from indx to plug in; they just implement the protocol. The protocol names are stable contracts: Parser, LLM, VLM, Embedder, Store, OutputWriter (and the Stage protocol that the pipeline itself uses). See the full signatures in the protocols reference.
This is also why a community package like indx-weaviate can ship a new store, advertise it through a Python entry point, and have store = "weaviate" simply work — with no fork of indx. First-party builtins always win on name collisions, so a plugin can never silently shadow a default.
3. Light core
Section titled “3. Light core”pip install indx stays small and fast. The core depends only on Typer, Rich, Click, Pydantic v2, and pydantic-settings (TOML parsing is stdlib). Every heavy backend — Docling, Torch, vector-DB clients, cloud SDKs — is an optional extra, never a core dependency.
| Extra | Installs |
|---|---|
indx[docling] | the default Docling parser |
indx[bge] | local bge-m3 embeddings (FlagEmbedding + Torch) |
indx[qdrant] | the Qdrant client |
indx[openai] / indx[anthropic] | cloud LLM SDKs |
indx[local] / indx[defaults] | the recommended local-first stack in one line — the two names are aliases for the same bundle |
indx[all] | every backend, for convenience or CI |
If you select a backend whose extra is not installed, indx fails fast with an actionable error naming the exact pip install "indx[...]" to run — it never breaks an unrelated code path. See the extras reference for the full matrix.
Two ways to select a component
Section titled “Two ways to select a component”A slot can be filled either by name (in config or on the CLI) or by object (in code). The two are equivalent — the name simply resolves through the registry to a class.
By name — config or CLI
Section titled “By name — config or CLI”In indx.toml, each section names the backend for a slot. Names resolve through the registry to the right implementation:
[parser]backend = "docling" # docling | unstructured | llamaparse | markitdown | custom
[llm]backend = "ollama" # ollama | vllm | openai | anthropic | azuremodel = "qwen2.5"# api_key comes from $INDX_LLM__API_KEY, never the file
[embedding]backend = "bge-m3" # bge-m3 | e5 | openai | cohere
[store]backend = "qdrant" # qdrant | pgvector | chroma | lancedb | jsonl
[output]writer = "indx" # indx | jsonl | langchain | llamaindexThe same names work as CLI flags, which take precedence over the config file:
indx ./docs --out ./ai-ready --parser markitdown --store jsonl --format langchainPrecedence is CLI flag > indx.toml > built-in default. See the configuration guide and the configuration reference for the complete schema.
By object — code
Section titled “By object — code”In the SDK, pass a constructed component straight into the pipeline. This is the same selection, just expressed as an object instead of a string:
from indx import DirectoryPipelinefrom indx.parsers import DoclingParserfrom indx.store import QdrantStore
# fully local, on-prem — nothing leaves the buildingpipeline = DirectoryPipeline( parser=DoclingParser(), llm="ollama:qwen2.5", embedder="bge-m3", store=QdrantStore(url="http://localhost:6333"),)space = pipeline.run("./docs", "./ai-ready")Custom objects only need to satisfy the protocol
Section titled “Custom objects only need to satisfy the protocol”Because slots use structural typing, your own class drops straight in as long as it matches the protocol — no base class, no registration ceremony:
from pathlib import Pathfrom indx.core import ParsedDoc, SpaceContext
class MyParser: def parse(self, source: Path, ctx: SpaceContext) -> ParsedDoc: ...
pipeline = DirectoryPipeline(parser=MyParser())For the full walkthrough of building components and stages of your own, see custom components and the protocols reference.
Why this matters
Section titled “Why this matters”A folder already encodes how an organization thinks — what sits next to what, what supersedes what, what points where. indx keeps that map. BYOS makes sure the map is yours: built on a light core, runnable entirely offline, and free of any single vendor’s roadmap. Whether you run on a laptop, a GPU server, or an air-gapped enterprise network, the same pipeline code applies — only the slots change.
Next, explore the core objects that flow through these slots, or the pipeline and stages that drive them.