Skip to content

Configuration Reference

giulio d'erme edited this page Aug 10, 2026 · 3 revisions

Configuration Reference

Every RECALL_* environment variable, grouped by what it protects.

Default values are not listed here. They live in .env.example, which is generated alongside the code and cannot drift from it. This page covers what each variable is for, when you would change it, and what breaks if you get it wrong — which is the part a config dump cannot tell you.

Precedence: real environment → .env file → built-in default. The .env loader never overrides a variable that is already set.


Environment mode

RECALL_ENV

Selects development, test or production behavior.

When you'd change it: set it explicitly to production on production processes.

If you get it wrong: anything other than the exact production value leaves production-only guards off. In production, search and forget use generation-routed serving, local filesystem indexing is refused, static token files are refused, pinned embedder identity is required for generations, and promotion is blocked from the ordinary runtime path. Full table: docs/MIGRATIONS.md.

Connection

RECALL_SERVING_DSN

The unprivileged runtime PostgreSQL connection string for indexing, search, forget and MCP.

When you'd change it: always, for anything but the local dev container.

If you get it wrong: the repo publishes default credentials in its docker-compose.yml, and using them against a non-local host makes your memory corpus readable by anyone who can reach the port. The MCP server therefore refuses to boot in that situation rather than warning — see RECALL_ALLOW_INSECURE_DSN. The CLI prints a loud stderr warning instead, because a CLI has no listener to protect.

RECALL_MIGRATION_DSN

The schema-owner credential used by recall schema apply and migration-only enterprise commands. The MCP process must not need it.

When you'd change it: whenever you provision or upgrade schema.

If you get it wrong: a runtime process with DDL ownership can mutate objects it should only read or write as data, and a readiness check run against the migration role certifies the wrong role's row-level-security behavior. Keep serving and migration credentials distinct outside a disposable database. → docs/MIGRATIONS.md

RECALL_DSN

Deprecated development fallback for the serving DSN, and a compatibility fallback for migration commands when RECALL_MIGRATION_DSN is absent.

RECALL_ALLOW_INSECURE_DSN

Accepts the risk above deliberately.

When you'd change it: you genuinely intend to run with published credentials — a throwaway environment, a demo on a trusted network.

If you get it wrong: you have re-enabled exactly the failure the refusal exists to prevent, and nothing will tell you again.

RECALL_POOL_SIZE

Selects the connection mode, which is a bigger switch than it looks.

Unset means one long-lived connection: correct for a CLI or any single-threaded caller, and the mode the store's reconnect semantics are built around. Set to an integer means a connection pool: what a server needs, so concurrent callers actually proceed concurrently.

When you'd change it: any server process. Requires the pool extra.

If you get it wrong: sharing a single connection across threads serialises them — the MCP server previously served exactly one request at a time for this reason. In the other direction, a pool starts a background maintenance thread that a one-shot CLI invocation should not pay for or have to shut down.

RECALL_STATEMENT_TIMEOUT_MS

Bounds every statement server-side.

When you'd change it: raise it if legitimate queries are being cancelled; lower it if you need tighter tail latency.

If you get it wrong: without a bound, a single runaway query occupies a connection until the process dies, with nothing able to cancel it. That is the difference between a slow request and an exhausted pool. Schema DDL is the one documented exception — it lifts the bound, because an HNSW build legitimately outlasts any sane query timeout, and restores it afterwards.

RECALL_SCHEMA_LOCK_TIMEOUT_MS

How long schema DDL may wait for a lock before giving up.

Read this carefully: it is not a bound on the work. An index build is deliberately unbounded. It bounds only queueing behind another transaction.

When you'd change it: set it to wait forever if you have a deployment that legitimately contends on this and you would rather block than retry.

If you get it wrong: with an unbounded wait, a concurrent transaction turns store startup into a silent stall — which presents as a hang, not an error. The DDL is idempotent and retried on the next store open, so failing fast loses nothing and is diagnosable where a stall is not.


Embedding

RECALL_EMBEDDER

Which embedder to use — the local ONNX model or the fully-offline hashing embedder.

When you'd change it: the hashing embedder for tests, CI and offline work; it is deterministic and needs no model download. Never for retrieval quality — it is weak on purpose.

If you get it wrong: the embedder's name is part of both the embedding cache key and the calibration file, so switching cannot silently reuse the previous model's vectors or its threshold. What you will see is a full re-embed on the next index and an uncalibrated fallback until you re-run calibrate. Both are correct; neither is silent.

RECALL_CALIBRATION

Path to the per-embedder calibration file written by recall calibrate.

When you'd change it: you keep calibrations per environment or per corpus.

If you get it wrong: a missing, unreadable, malformed, out-of-range, or wrong-embedder calibration is ignored, with an uncalibrated fallback and calibrated=False flagged in every result. That defensiveness is deliberate — a corrupt file must never be able to silently disable abstention, and a threshold fitted in another model's cosine regime must never be applied. → The-Trust-Layer

Legacy local calibration files are import-only evidence in v1 generation serving. The MCP server does not automatically select calibration.json; it resolves calibration from PostgreSQL for the authenticated tenant and active generation.

RECALL_EMBED_PROFILE · RECALL_MODEL_CACHE · RECALL_MODEL_SHA256

Select and verify a registered immutable embedding profile for generation serving. Registered profile identifiers live in recall/embedding_registry.py.

When you'd change them: production or enterprise deployments that need a pinned local model artifact and reproducible embedding identity.

If you get them wrong: startup refuses before loading the model when the artifact is missing or its tree digest does not match. A profile is more than a model name: it includes dimension, query and passage encoder modes, context mode, normalization and policy versions.


Indexing bounds

Two different jobs here, and conflating them is the usual mistake: per-request caps bound one call; budgets bound the aggregate per tenant, so a client staying politely under the per-request cap cannot simply issue it in a loop.

RECALL_INDEX_ROOT

Bounds where the MCP recall_index tool may read from.

When you'd change it: narrow it to exactly the corpus directory you intend to expose.

If you get it wrong: a client-callable indexer with no root confinement can read anything the server process can. The confinement survives symlinks.

RECALL_INDEX_MAX_FILES · RECALL_INDEX_MAX_BYTES

Per-request caps on candidate file count and candidate bytes. Checked before anything is embedded, so an oversized request costs nothing.

When you'd change it: larger corpora indexed in one call; or tighter, if your embedder is a paid API.

If you get it wrong: a client-callable indexer with no cap is unbounded spend on a cloud embedder.

RECALL_INDEX_BYTES_PER_HOUR

The aggregate indexing budget, per tenant, per hour. This is the load-bearing one: request count is a poor proxy for spend when one call can carry twenty megabytes and the next two hundred bytes.

When you'd change it: matching it to what you are willing to pay.

⚠️ Keep it at or above RECALL_INDEX_MAX_BYTES. If the hourly budget is smaller than the per-request cap, requests sized between the two can never succeed — they pass the per-request check and are then refused by a budget they cannot fit into.

RECALL_MAX_PRUNE_FRACTION

The fraction of a root's indexed sources that a re-index may delete before it refuses and deletes nothing.

When you'd change it: rarely. Lower it if your corpus should never shrink much.

If you get it wrong: set too high, a missing corpus — wrong path, unmounted volume, half-finished checkout — becomes indistinguishable from a deleted one, and a single bad run empties your index. The guard also applies above a small floor of indexed sources, so tiny corpora are not constantly tripping it. Override per-run with --allow-prune once you have confirmed the files really are gone.


Retrieval / ANN

RECALL_RETRIEVAL_PROFILE

Selects a fixed process retrieval profile: unset legacy behavior, fast, or quality.

When you'd change it: run separate processes for low-latency and reranked quality traffic.

If you get it wrong: clients cannot choose the expensive path per request. The process profile sets candidate pool size, returned count, reranking, admission budget, concurrency and queue capacity. Contradicting it with the legacy RECALL_RERANK switch refuses startup.

fast uses no reranker and a tight admission budget. quality uses the pinned local reranker and a larger budget. Requests that cannot start within the budget are shed before the query is embedded, so a refused request spends no embedding or reranking work.

RECALL_RERANK · RECALL_RERANK_MODEL · RECALL_RERANK_REVISION

Legacy opt-in cross-encoder reranking path when no fixed retrieval profile is set.

RECALL_RERANK is parsed as a boolean and malformed values are refused. Overriding the model requires a revision because an unpinned Hub reference is mutable.

RECALL_RERANK_PATH · RECALL_RERANK_SHA256 · RECALL_RERANK_THREADS

Quality-profile reranker artifact settings.

The path is deployment-specific. The digest must match the value pinned by the package, and the artifact is loaded locally. RECALL_RERANK_THREADS applies to the quality profile only; it is not read on the legacy RECALL_RERANK path.

RECALL_HNSW_EF_SEARCH_FILTERED · RECALL_HNSW_ITERATIVE_SCAN_FILTERED

pgvector search parameters applied to the source-filtered query path only.

Why they exist: an HNSW walk is filter-blind, so at pgvector's defaults a selective filter exhausted the candidate list before finding k matches and the query silently returned fewer results than requested. Widening the search and relaxing iterative scan eliminates that truncation, at a latency cost.

⚠️ This is a correctness fix, not a recall improvement — and the distinction is measured, not rhetorical. relaxed_order fills to k with approximate matches, so you are trading truncation for approximation. On one corpus recall improved; on a corpus built the way a real index run builds one it moved slightly the other way. Both figures → FINDINGS.

When you'd change them: tuning the latency cost, or reverting to exact-but-truncating behaviour if approximate fill is worse for your use than short results.

Stated honestly: the unfiltered path still runs at pgvector's defaults, where it measured well — but every query now also carries a tenant predicate, and as of the README's last revision that combination had not been measured on a multi-tenant table. Background: issue #11. Check the README for the current status.


Multi-tenancy

RECALL_TENANT

The tenant namespace. A store is bound to one tenant for its lifetime — tenancy is a property of which store you hold, not an argument you pass to a query.

When you'd change it: operating on a different tenant's memory. Note this applies to stdio only; over HTTP the token carries the tenant, and this variable is ignored.

If you get it wrong: the row-level-security policy means a wrong tenant returns nothing rather than someone else's data — provided you connect as an unprivileged role. → Tenancy-and-Auth


MCP transport and authentication

Full treatment: Tenancy-and-Auth and docs/AUTH.md.

RECALL_TRANSPORT

stdio (a private pipe to one client) or an HTTP transport (a network listener).

If you get it wrong: the two have deliberately different security postures. stdio needs no authentication because there is no remote caller — the pipe is the boundary. An HTTP transport opens a socket, and starting one without tokens refuses to boot.

RECALL_AUTH_TOKENS_FILE

The only source of bearer tokens.

Note what is absent: there is no environment variable that accepts a raw token, and the omission is the point. Environment variables leak through /proc/<pid>/environ, ps e, container inspection APIs, crash dumps, and every child process the server spawns. A file is what Kubernetes and Docker mount for secrets anyway, and unlike an env var it can be permission-checked — which the server does, warning if the file is world-readable.

The file may hold plaintext tokens or their SHA-256 digests, so provisioning access never requires writing a live credential to disk in recoverable form.

RECALL_AUTH_ISSUER_URL · RECALL_AUTH_RESOURCE_URL

Identity metadata advertised to MCP clients for the protected-resource auth flow.

RECALL_AUTH_MODE

Used only during a staged cutover when static token and OIDC configuration are both present. Without an explicit mode, both-set refuses to boot. static keeps the token file active; oidc switches to the OIDC provider.

RECALL_OIDC_ISSUER · RECALL_OIDC_AUDIENCE · RECALL_OIDC_TENANTS

OIDC authentication for production HTTP transports. RECALL_OIDC_TENANTS is mandatory and bounds which tenant namespaces this deployment serves.

RECALL_OIDC_SUBJECT_TENANTS · RECALL_OIDC_TRUST_TENANT_CLAIM

Exactly one is required with OIDC. Either bind subjects to tenants in this process, or declare that the identity provider mints the tenant claim from an authoritative subject-to-organization mapping. Setting both refuses to boot.

RECALL_OIDC_ALGORITHMS

Optional asymmetric algorithm allowlist. Symmetric and unsigned algorithms are refused because, with a published verification key, HMAC algorithms create an algorithm-confusion attack surface.

RECALL_RATE_READ_PER_MIN · RECALL_RATE_WRITE_PER_MIN · RECALL_RATE_FORGET_PER_MIN

Per-tenant call rates, split by the risk each tool class carries — searching and reading stats, indexing, and erasure.

Why per tenant and not per principal: two tokens issued to the same tenant are the same blast radius and the same bill. Letting a tenant multiply its budget by minting another token would make the quota advisory.

Each takes a number or the literal off. A malformed value, a non-finite one, or one too small to yield a non-zero rate falls back to its default rather than being read as "unlimited". A typo must not silently remove a cap.

⚠️ Buckets are per process. Nothing is shared across workers, so N server processes admit roughly N times these rates. This is honest for the deployment the auth work targets — one process behind TLS — and it is the first thing to revisit before running a fleet. A shared limiter needs Redis or the database, and a network round trip on every call.

⚠️ Read once at startup. Changing a budget takes effect on restart.


Logging

RECALL_LOG_LEVEL · RECALL_LOG_FORMAT

Level, and text or JSON output.

Important: these configure the CLI and the MCP server. The library itself never attaches logging handlers — that is the host application's job, and a library that calls basicConfig hijacks its host's logging configuration. If you embed recall in your own service, configure logging yourself; these variables will not do it for you.

RECALL_HOST · RECALL_PORT

Bind address for the HTTP transports.


Reading this list operationally

If you change one thing before going to production, make it RECALL_SERVING_DSN — specifically, the role it connects as. Row-level security is bypassed by a superuser or a BYPASSRLS role, which means the second isolation layer is decoration unless you connect as an unprivileged one. store.check_rls_effective() tells you which you have. → Tenancy-and-Auth

If you change two, add RECALL_POOL_SIZE for any server process.

Clone this wiki locally