KHORA_ or a KhoraConfig instance constructed programmatically. Both paths are backed by the same pydantic-settings model in src/khora/config/schema.py.
Two ways to configure
Environment variables
All settings use theKHORA_ prefix with single-underscore separators for nested fields. Examples:
KHORA_STORAGE__GRAPH__URL) is still accepted as a backwards-compatible alias on every nested-config field. New code and .env files should use the single-underscore form shown throughout this document. The legacy form continues to work but is no longer documented.
Nested-object env vars (graph backend, vector backend, dream-phase per-op toggles) are documented in the Nested env vars section below.
Programmatic
Install extras
Combine extras as needed:
pip install 'khora[rust,otel]'. See Observability for the full description of open telemetry env-var contract, precedence rules, and vendor recipes. Khora always exposes the OTel API. The [otel] and [logfire] extras determine where spans/metrics go.
Core settings
Storage
Prefix:KHORA_STORAGE_. See Storage backends for the full backend matrix.
Graph and vector backends nest under
storage.graph and storage.vector. The flat fields KHORA_STORAGE_NEO4J_URL, KHORA_STORAGE_NEO4J_USER, KHORA_STORAGE_NEO4J_PASSWORD, KHORA_STORAGE_PGVECTOR_URL, and KHORA_STORAGE_EMBEDDING_DIMENSION remain supported as a back-compat path and are migrated into the discriminated-union configs automatically.
Neo4j pool metrics
With any OTel backend installed ([otel] or [logfire]), the Neo4j backend emits OTel metrics automatically. See Observability. For high-frequency sub-minute sampling enable:
Neo4j relationship limits
Relationship.source_document_ids and Relationship.source_chunk_ids are append-bounded on every MERGE to prevent unbounded growth on hot edges. Defaults are 100 and 250 respectively. For deep-provenance workloads, where many documents contribute to the same edge, raise the relevant knob and watch the khora.neo4j.relationship.source_id_truncated metric:
logger.warning(...) records the field name, dropped count, rows affected, and configured limit.
Embedded backends
The embeddedsqlite_lance path is appropriate for demos, evaluation, tests, and small single-user CLIs. It is not the deployment story. For production, use PostgreSQL + pgvector + Neo4j.
Documented scale ceiling, where performance and recall degrade noticeably above these thresholds:
- ~1M chunks (LanceDB IVF-PQ training time + write serialisation start to dominate)
- ~100k entities (recursive-CTE traversal cost on hub nodes)
- ~500k relationships
- Traversal depth ≤3 (the
instr(walk.visited, ...)visited-set scan ingraph.pyisO(depth × fan-out × visited-len)and degrades sharply at depth ≥4 with high fan-out)
- Partial atomicity in
coordinator.transaction(): only the SQL session is enrolled. LanceDB writes happen post-commit with compensating-delete-on-failure. A crash between SQLite commit and Lance write can leave orphaned vectors or missing embeddings, and reconciliation runs on the next ingest. - Point-in-time queries degrade (they don’t raise) on the embedded stack. A
target_datequery no longer raisesNotImplementedError: entity-version narrowing is skipped (current-state entities are returned) and aDegradationis recorded onRecallResult.engine_info["degradations"]. Occurred-time bounds (start_time/end_time) still narrow chunks normally. The bi-temporal entity versioning that powers true point-in-time lives in Neo4j (version_valid_from/version_valid_toon:Entity/:EntityVersionnodes), whichsqlite_lancehas no equivalent of. - FTS5 covers chunks only: entity-anchored recall falls back to
LIKE/ JSON-equality. Recommend the PostgreSQL stack for entity-heavy corpora. - Install footprint is ~130–180 MB unpacked (pyarrow + lancedb native + Arrow C++ runtime). “Embedded” means “no server”, not “no native deps”.
- IVF-PQ retraining is automatic when the corpus grows past
retrain_factor × (rows at last training). Tune viaKHORA_STORAGE_SQLITE_LANCE_RETRAIN_FACTOR.
sqlite_lance storage sub-config. See the KHORA_STORAGE_SQLITE_LANCE_* table below for DB_PATH, LANCE_PATH, EMBEDDING_DIMENSION, USE_HALFVEC, LANCE_INDEX, IVF_PARTITIONS, HNSW_M, and RETRAIN_FACTOR with defaults and tuning guidance.
LLM
Prefix:KHORA_LLM_. LiteLLM handles the provider dispatch.
For multi-model routing, LiteLLM’s router is configurable via
config_file (path to a
LiteLLM config YAML), model_list, and router_settings on KhoraConfig.llm.
Pipeline (extraction)
Prefix:KHORA_PIPELINES_.
Query
Prefix:KHORA_QUERY_. See Retrieval for guidance.
Two reranking variables don’t reach
recall(). The default VectorCypher engine reconciles the query.reranking_* family onto its own config, so the variables above apply to it. KHORA_QUERY_RERANKING_METHOD and KHORA_QUERY_RERANKING_FINAL_K are the exception: they exist on QuerySettings but have no VectorCypherConfig equivalent, so they affect only the separate HybridQueryEngine. Setting them changes nothing on the default recall path. For per-engine overrides and model choice, see Reranking.Abstention
When recall finds nothing on-topic, Khora can flag the result rather than return weak matches (see Retrieval). The scoring is tunable:weighted mode also exposes per-signal weights (ABSTENTION_WEIGHT_ENTITIES_EMPTY, ABSTENTION_WEIGHT_CHUNKS_BELOW_MIN, ABSTENTION_WEIGHT_TOP_SCORE_LOW) and confidence targets (ABSTENTION_CONFIDENCE_TARGET_COSINE, ABSTENTION_CONFIDENCE_TARGET_GAP); the three weights must sum to ≤ 1.0.
Experimental query knobs
These are real but opt-in and not yet validated (default off, A/B benchmarking pending upstream). Documented for completeness. Leave them at their defaults unless you’re running your own evaluation.Telemetry
Khora has two independent telemetry paths. Spans and metrics (OpenTelemetry). Khora emits spans (@trace, trace_span()) and metrics through the OpenTelemetry API unconditionally. Whether they’re exported depends only on which TracerProvider / MeterProvider is installed, not on any KHORA_* variable. Install the [otel] extra and call configure_telemetry() (honors OTEL_* env vars), or install [logfire] and run logfire.configure(), and khora’s signals flow to your collector. With no provider configured, OTel returns a NonRecordingSpan and the helpers are near-free. See Observability for the full setup and the OTLP env-var contract.
Structured event log (PostgreSQL). Separately, khora can write structured LLMEvent / StorageEvent / PipelineEvent rows to a PostgreSQL table. This is opt-in and independent of the OTel path above.
Logging
Khora uses loguru. Callkhora.logging_config.setup_logging() once per process (or configure your own sinks with enqueue=True). See the Logging section of the khora CLAUDE.md for the full rationale. Short version: default loguru sinks are synchronous and will block an asyncio event loop on every logger.* call.
Secrets
Secret parameters, like API keys (OpenAI, Anthropic, etc.) are read from the environment variable named byKHORA_LLM_API_KEY_ENV (default OPENAI_API_KEY). Khora never reads credentials from disk. They come from the environment. Credentials are read once when KhoraConfig is constructed and bound into the connection pools and the LLM client at startup, so rotating a secret takes effect on the next process start (or whenever you rebuild the config and reconnect). There’s no in-process reload.
Credential fields
Credential fields onKhoraConfig (PostgreSQL DSN, Neo4j password, LLM API key, telemetry DSN, etc.) are pydantic.SecretStr. This has two operator-visible consequences:
repr()and config-dump output render the value as'**********'. Logs, error messages, andKhoraConfig().model_dump()do not leak cleartext credentials.- Code that reads the cleartext value must call
.get_secret_value()explicitly. SQLAlchemy engines and graph drivers receive the cleartext at the boundary. Downstream library consumers must do the same.
Lockfile policy
khora’spyproject.toml includes [tool.uv] exclude-newer = "7 days", a relative, evaluated-on-every-sync guard against pulling brand-new upstream releases that haven’t had time to stabilise. Security-critical packages opt out via exclude-newer-package (currently only urllib3 for CVE-2026-44431 / CVE-2026-44432). Downstream consumers that mirror khora’s pin policy inherit the same 7-day staging window for transitive dependencies; override per-package as needed.
Nested env vars
Reference for every Khora environment variable that lives on a sub-object attached to a sub-settings class: graph backend, vector backend, the SQLite+LanceDB embedded stack, and the dream-phase per-op toggles.Spelling. All env vars in this section use single underscore between every level:KHORA_STORAGE_GRAPH_URL, notKHORA_STORAGE__GRAPH__URL. The legacy double-underscore form continues to work as a backwards-compatible alias on every nested-config field. It is no longer documented. New code and.envfiles should use the single-underscore form.
Neo4j graph backend
Configuration for the Neo4j graph backend (storage.graph).
Vector backend
Discriminated union overPgVectorConfig | SQLiteVectorConfig, keyed by backend. Default is pgvector via default_factory=PgVectorConfig.
For
backend=sqlite, only BACKEND / URL / EMBEDDING_DIMENSION are model-exposed.
Embedded backend
Used whenKHORA_STORAGE_BACKEND=sqlite_lance. Pairs an on-disk SQLite database (graph + relational + event store) with a sibling LanceDB directory (vector search). Zero infrastructure: both backends run in-process.
Dream-phase per-op toggles
DreamConfig.ops: DreamOpsConfig carries per-operation enable flags. Every destructive op defaults to false. KHORA_DREAM_ENABLED=true alone runs no destructive work, and each op must be flipped explicitly.
See Dream phase for operational guidance, retention floors, and the kill-switch (KHORA_DREAM_DISABLE_APPLY).
Env vars that are not nested
These reach top-level fields of each sub-settings class via the sub-class’s ownenv_prefix. There is no sub-object hop, and they’re covered in the sections above:
KHORA_STORAGE_BACKEND,KHORA_STORAGE_POSTGRESQL_*,KHORA_STORAGE_HNSW_*,KHORA_STORAGE_USE_HALFVEC: flat onStorageSettings.KHORA_LLM_*: every field onLLMSettingsis top-level (no sub-objects).KHORA_PIPELINES_*: every field onPipelineSettingsis top-level.KHORA_QUERY_*: every field onQuerySettingsis top-level.KHORA_TELEMETRY_*: flat.KHORA_DREAM_*(e.g.KHORA_DREAM_ENABLED,KHORA_DREAM_DEFAULT_MODE,KHORA_DREAM_LLM_MAX_TOKENS_PER_RUN): flat onDreamConfig. Only the per-op toggles under theops:sub-object are listed above.