> ## Documentation Index
> Fetch the complete documentation index at: https://docs.suchi.page/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> Every knob, its default, its purpose. Env vars + optional config file.

## Precedence

suchi reads configuration from two layers, last wins:

1. **Config file** (optional overlay — see below)
2. **Process environment variables**

Env values always beat file values. If a knob is set in both places,
the env wins — safe for docker-compose overrides where the file lives
in an image layer and the env comes from the run command.

## Config file (optional)

**Recommended format: TOML.** Explicit typing, no indentation
surprises, no parse ambiguity. suchi also accepts HUML, YAML, and
JSON — but TOML is what the examples use.

Format is picked from file extension:

| Extension        | Parser                                                  |
| ---------------- | ------------------------------------------------------- |
| `.toml`          | **recommended** — `github.com/BurntSushi/toml`          |
| `.huml`          | [HUML](https://huml.io) — TOML-adjacent, human-friendly |
| `.yaml` / `.yml` | `gopkg.in/yaml.v3`                                      |
| `.json`          | stdlib `encoding/json`                                  |

> **Why TOML over YAML:** YAML has the Norway problem (bare `no` parses
> as boolean false), sexagesimal time gotchas, and `1.10`-decimal-vs-
> string ambiguity. TOML has none of these. If you use YAML anyway,
> **quote string values** to defuse the surprises.

### Search paths

First readable file wins:

1. `$SUCHI_CONFIG` — explicit path override (highest priority)
2. `$XDG_CONFIG_HOME/suchi/config.<ext>`
3. `$HOME/.config/suchi/config.<ext>`
4. `/etc/suchi/config.<ext>`
5. `./suchi.<ext>` (cwd)

Within each directory, extensions are probed in the order **toml →
huml → yaml/yml → json**.

### File shape

Keys mirror env var names in lower\_snake. `PUBLIC_URL` becomes
`public_url`; `BODY_LIMIT` becomes `body_limit`. Nested tables
flatten with `_`:

```toml theme={null}
[oidc]
issuer = "https://id.example.com"
client_id = "suchi"
```

is equivalent to:

```
OIDC_ISSUER=https://id.example.com
OIDC_CLIENT_ID=suchi
```

Arrays become CSV strings: `ocr_languages = ["eng", "deu"]` matches
`OCR_LANGUAGES=eng,deu`.

Full samples:

* [`config.example.toml`](https://github.com/suchi-dms/suchi/blob/main/docs/config.example.toml) (recommended)
* [`config.example.huml`](https://github.com/suchi-dms/suchi/blob/main/docs/config.example.huml)
* [`config.example.yaml`](https://github.com/suchi-dms/suchi/blob/main/docs/config.example.yaml)

`SUCHI_CONFIG` opts operator into a specific file bypassing the
search order — useful for multi-environment deployments where prod
and staging point at different TOMLs.

## Environment variables — the authoritative knob list

## Required

| Env var      | Purpose                                                                                                               |
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_URL` | External URL. Used for cookies, OIDC callbacks, share-link URLs. Must be the URL your reverse proxy exposes suchi at. |

## Storage & runtime

| Env var                           | Default                  | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DATA_DIR`                        | `/data`                  | Root of `blobs/`, `suchi.db` + WAL, `backups/`, `config.yaml`, `staging/`.                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `LISTEN_ADDR`                     | `:8000`                  | HTTP listener.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `LOG_LEVEL`                       | `info`                   | `debug` / `info` / `warn` / `error`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `UPLOAD_MAX_BYTES` / `BODY_LIMIT` | `500M`                   | Per-request body cap. Applies to every HTTP endpoint (upload, PATCH, JSON POSTs) **and** the fs-watch producer's file-size check. `0` disables the cap. Set the same value at both env names — `UPLOAD_MAX_BYTES` wins if both are present. Accepts `K`/`M`/`G` suffixes or raw bytes.                                                                                                                                                                                                                                          |
| `SESSION_KEY_FILE`                | `$DATA_DIR/.session-key` | Signed-cookie key. Auto-generated 0600 on first boot.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `BACKUP_INTERVAL`                 | `24h`                    | `VACUUM INTO` snapshot cadence. Writes `$DATA_DIR/backups/suchi-<ts>.db`. `0` disables.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `BACKUP_KEEP`                     | `7`                      | Retention — after each snapshot, prune all-but-the-latest N. `0` keeps everything (documented; not the default).                                                                                                                                                                                                                                                                                                                                                                                                                |
| `AUDIT_RETENTION_DAYS`            | `20`                     | Sliding window over `audit_events`. Rows older than this get pruned on the same tick as the backup snapshot. Range `0–100`; `0` disables (audit log grows forever). Directly bounds the storage growth path the `GET /api/events/` feed sits on.                                                                                                                                                                                                                                                                                |
| `OCR_LANGUAGES`                   | `eng`                    | Comma-separated tesseract langs; shortcut for the OCR plugin's config.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `PDF_MAX_CONTENT_BYTES`           | `8M`                     | Cap on pdftotext output length written to `documents.content`. Truncation is silent + logged at Warn — the doc still lands, FTS still indexes what fit.                                                                                                                                                                                                                                                                                                                                                                         |
| `EPUB_MAX_CONTENT_BYTES`          | `32M`                    | Cap on the concatenated XHTML text of an EPUB. Bigger than the PDF default because ebooks routinely exceed it (novels are fine, textbooks may still truncate).                                                                                                                                                                                                                                                                                                                                                                  |
| `DJVU_MAX_CONTENT_BYTES`          | `32M`                    | Cap on `djvutxt` output length. Same reasoning as EPUB.                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `OCR_ENGINE`                      | `auto`                   | Scanned-PDF OCR engine. `auto` picks `tesseract` when the tessocr binaries (`pdftoppm` + `tesseract`) are on PATH, else `ocrmypdf`. Explicit values force one engine. `tesseract` is \~4× smaller in the image but produces no searchable-PDF archive.                                                                                                                                                                                                                                                                          |
| `SCAN_BLANK_REMOVAL`              | `auto`                   | `auto` (default) → detect and strip blank pages from the working copy before OCR. `off` → skip. The CAS original is untouched either way.                                                                                                                                                                                                                                                                                                                                                                                       |
| `SCAN_BLANK_WHITENESS_THRESHOLD`  | `0.995`                  | Fraction of mean pixel intensity above which a page is considered blank. Range `(0.0, 1.0]`. Lower it (e.g. `0.98`) if your scanner leaves grey background; raise it (`0.999`) to only strip truly white pages.                                                                                                                                                                                                                                                                                                                 |
| `SCAN_SPLIT_ENABLED`              | `off`                    | `on` → detect QR separator sheets and fan out one child document per segment. Opt-in only. See [formats#multi-doc-splitting](/formats#multi-doc-splitting-on-qr-separator-sheets).                                                                                                                                                                                                                                                                                                                                              |
| `SCAN_SPLIT_TOKEN`                | `SUCHI-SPLIT`            | QR payload that marks a separator page. Case-sensitive; the value your `qrencode` (or equivalent) encodes on the printed separator.                                                                                                                                                                                                                                                                                                                                                                                             |
| `SCAN_SPLIT_DPI`                  | `150`                    | Rasterization DPI for QR detection. Range `[72, 600]`. Bump if your separator QRs are small or your scans are low-res.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `INGEST_PASSWORDS_FILE`           | (unset)                  | Optional path to a newline-separated list of candidate PDF passwords tried before an encrypted PDF is parked in `encryption_state='encrypted'`. Blank lines and `#` comments are skipped. Reloaded per-doc so operators can append passwords without restart. Chmod 600 recommended — the file holds secrets.                                                                                                                                                                                                                   |
| `DECRYPT_KEY_FILE`                | `$DATA_DIR/.decrypt-key` | Path to the AES-256-GCM key that seals operator-supplied passwords in the `decryption_passwords` table. Auto-generated 0600 on first boot. **Losing this file loses ALL stored passwords** — back up `$DATA_DIR` wholesale, not just `suchi.db`.                                                                                                                                                                                                                                                                                |
| `PRE_CONSUME_SCRIPT`              | (unset)                  | Optional path to an operator-defined executable that runs on every doc before any built-in ingest logic. See [preconsume](/preconsume) for the contract (argv, env vars, stdout JSON envelope).                                                                                                                                                                                                                                                                                                                                 |
| `SUCHI_UI_DISABLED`               | (unset)                  | Set to `1` or `true` to turn off the built-in browser UI at boot. When enabled, `/`, `/app/*` (Svelte SPA), `/login`, `/bootstrap`, `/preview/{id}`, `/download/{id}`, `/assets/*` don't register — only `/api/*`, `/healthz`, `/readyz`, `/metrics` are served. Blob access for headless callers keeps working via the `/api/documents/{id}/{preview,download}` mirrors (see [api](/api#get-apidocumentsidpreview)). For deployments where a self-hosted SPA fronts `/api/` externally. Auth (Token/Bearer/OIDC) is unchanged. |

## TLS

If a reverse proxy terminates TLS (recommended), leave these unset.
For proxy-less installs, set both:

| Env var         | Purpose          |
| --------------- | ---------------- |
| `TLS_CERT_FILE` | Cert file (PEM). |
| `TLS_KEY_FILE`  | Key file (PEM).  |

## Local auth

No config needed. On first boot with no admin user, suchi prints a
one-time **setup token** to the log at WARN. POST it to `/setup` with
an email + password to create the admin.

## OIDC

All-or-nothing group. Set the whole set or none:

| Env var                                           | Purpose                                                                                                           |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `OIDC_ISSUER_URL`                                 | e.g. `https://id.example.com`                                                                                     |
| `OIDC_CLIENT_ID`                                  | From your IdP.                                                                                                    |
| `OIDC_CLIENT_SECRET` or `OIDC_CLIENT_SECRET_FILE` | The secret. `_FILE` variant reads from disk (docker/k8s secrets).                                                 |
| `ADMIN_EMAIL`                                     | Required when OIDC is enabled. The email that gets admin role on first login. Everyone else defaults to `member`. |

<Info>
  Any OIDC provider works: Pocket-ID, Authentik, Keycloak, Microsoft
  Entra, Google.
</Info>

## Filesystem-watch ingest (Phase 2)

| Env var                 | Default                               | Purpose                                                                                                                                             |
| ----------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INGEST_FS_OWNER_EMAIL` | (unset = idle)                        | User that owns docs picked up from the staging dir. Setting this activates the watcher.                                                             |
| `INGEST_FS_DIR`         | `$DATA_DIR/staging` when owner is set | Watch dir. Files landed here (via move, scp, drag-drop) become docs. Sidecars (`<name>.json`) apply metadata. Failed files land in `<dir>/errors/`. |

## Email ingest (IMAP polling)

| Env var                                               | Purpose                                                                                                                                                                                     |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INGEST_IMAP_URL`                                     | `imaps://user@imap.fastmail.com/INBOX` etc. Unset = email ingest idle. `imap://` for cleartext (LAN / Bridge only), `imaps://` for TLS. Port optional; defaults to 993 (TLS) / 143 (plain). |
| `INGEST_IMAP_PASSWORD` or `INGEST_IMAP_PASSWORD_FILE` | Mailbox password / app password. `_FILE` variant reads from a mounted secret.                                                                                                               |
| `INGEST_IMAP_OWNER_EMAIL`                             | Owner user for imported docs. Falls back to `INGEST_FS_OWNER_EMAIL` when unset.                                                                                                             |

**How it works.** Every poll interval (default 5 min), suchi opens
the folder, `UID SEARCH UNSEEN`, fetches each message's raw body,
stores it in the CAS as `message/rfc822`, and enqueues a post-ingest
job. `core/pipeline/eml` then fans out one child document per
attachment. Successfully imported messages are marked `\Seen` (or
moved to `ProcessedFolder` when configured).

**Dedup** is by `Message-ID` — a doc with the same Message-ID under
the same owner is skipped on re-polls. Safe across restarts + across
transports (the same email that also arrived via mbsync + fs-watch
won't double-ingest).

**Bridge tip.** If you're pointing at Proton Bridge, use
`imap://protonmail-bridge:143` (Bridge doesn't do TLS internally —
docker network isolates it) and provide the Bridge-generated app
password via `INGEST_IMAP_PASSWORD_FILE=/run/secrets/imap-password`.

## LLM classifier (optional; egress on)

Off by default. Setting these turns the classifier on; non-local
endpoints require the egress ack.

| Env var                             | Purpose                                                                                                                                                 |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LLM_ENDPOINT_URL`                  | e.g. `http://127.0.0.1:11434/v1` for Ollama, or an OpenAI-compatible URL. Unset = classifier disabled.                                                  |
| `LLM_MODEL`                         | Model identifier passed to the endpoint (e.g. `llama3.1:8b`).                                                                                           |
| `LLM_API_KEY` or `LLM_API_KEY_FILE` | Optional; blank for local Ollama, required for hosted endpoints.                                                                                        |
| `LLM_EGRESS_ACK`                    | Must be `true` for non-local endpoints. Explicit acknowledgement that outbound traffic is intentional. Local endpoints (127.0.0.1, ::1) skip the check. |

The setup wizard writes these into the `settings` table so live-reload
works without a restart. Settings win over env when set.

## Mail-mbsync sidecar (optional wizard)

The mail-mbsync compose bundle in `deploy/mail-mbsync/` pairs suchi
with an IMAP-poll sidecar. Credentials are written into an env file
that the sidecar reads.

<Note>
  The server-rendered wizard at `/admin/mail-setup` retired with the
  SPA promotion; `/admin/mail-setup` now redirects here. An in-app
  mail-settings panel is tracked (needs `GET/PUT /api/admin/settings/mail`

  * a connection-test endpoint) — until it lands, populate the env
    file below by hand or via your deploy tool.
</Note>

| Env var                  | Default                | Purpose                                                                               |
| ------------------------ | ---------------------- | ------------------------------------------------------------------------------------- |
| `MAIL_SETUP_ENV_PATH`    | (unset)                | Writable path to the sidecar's env file. Unset = wizard disabled (route returns 404). |
| `MAIL_SETUP_CONTAINER`   | `suchi-mail-mbsync`    | Docker container to restart after the wizard writes. Empty = no auto-restart.         |
| `MAIL_SETUP_DOCKER_SOCK` | `/var/run/docker.sock` | Docker Engine socket used for restarts. Ignored when the container name is empty.     |

## MCP client env (for `suchi mcp` / `suchi-mcp` symlink)

The MCP subcommand acts as a client to a running suchi instance —
these tell it how to reach the API. Flags override env.

| Env var       | Purpose                                                              |
| ------------- | -------------------------------------------------------------------- |
| `SUCHI_URL`   | Base URL, e.g. `http://127.0.0.1:8000`. Also honored by `--url`.     |
| `SUCHI_TOKEN` | Scoped API token from `POST /api/token/`. Also honored by `--token`. |

## Config file loader itself

| Env var           | Purpose                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------- |
| `SUCHI_CONFIG`    | Explicit override for the config file path. Bypasses the search order entirely.                      |
| `XDG_CONFIG_HOME` | Standard XDG variable; feeds the search path. Falls back to `$HOME/.config` when unset.              |
| `HOME`            | Standard shell variable; the config search reads `$HOME/.config/suchi/config.*` as one of the paths. |

## Settings-only knobs (in-DB, not env-driven)

These live in the `settings` table and are set via the admin UI's
setup wizard rather than env vars. Settings win over env for the
handful of keys that overlap (LLM, ingest fs-watch, OCR languages).

| Key                                                                        | Purpose                                                                   |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `backup.interval_hours`                                                    | Backup cadence in hours (settings-win alternative to `BACKUP_INTERVAL`).  |
| `ingest.fs_watch_dir` / `ingest.fs_watch_owner`                            | Wizard-written overrides for the fs-watcher.                              |
| `jd.preset`                                                                | JD taxonomy mode (`jd` or `flat`).                                        |
| `llm.endpoint_url` / `llm.model` / `llm.api_key_sealed` / `llm.egress_ack` | LLM classifier config. `_sealed` is AEAD-encrypted with the settings key. |
| `ocr.languages`                                                            | JSON array form of `OCR_LANGUAGES`.                                       |
| `setup.completed_at` / `setup.steps_done` / `setup.steps_skipped`          | Wizard progress.                                                          |

## Secrets convention

Every secret env var has a `_FILE` variant. The file's contents (trimmed
of trailing newlines) becomes the value. Matches docker + k8s secret
mounts. Secrets are never logged. Session key sits at 0600.

Confirmed `_FILE` variants today:

* `OIDC_CLIENT_SECRET_FILE`
* `INGEST_IMAP_PASSWORD_FILE`
* `LLM_API_KEY_FILE`

If both the plain and `_FILE` variants are set, `_FILE` wins.

## Egress surface

<Note>
  A stock install makes **zero outbound connections** (principle 8).
  At boot, suchi logs `main.egress.surface` listing every configured
  outbound path — OIDC discovery, IMAP polling, cloud LLM endpoints,
  export sinks. `outbound: none` is the honest default.
</Note>

See [privacy](/privacy) for the full posture.
