# Configuration

> Every setting in config.toml and every environment variable the Kribble server reads, with defaults

`kribble-web` and `kribble-builder` read one TOML file. Give both the same file. This page lists every setting, section by section.

## The file

- **Path.** `KRIBBLE_CONFIG_PATH`, or `config.toml` in the working directory if it is not set. The units written by `install.sh` use `/opt/kribble/config.toml`. In the Docker image it is `/app/config.toml`.
- **No environment overrides.** Settings come from the file only. The environment variables at the end of this page control logging and a few helpers, not settings.
- **Changes take effect on restart** of both services: `sudo systemctl restart kribble-web kribble-builder`, or `docker compose restart kribble-web kribble-builder`.
- **Secrets.** The file holds database, storage, SMTP and API credentials. Keep it owned by the service user with mode `600` (`install.sh` sets this).
- **Defaults.** A missing key, an empty string and `0` all mean "use the default". You can't set a number to `0` this way.

Kribble refuses to start with exit status 2 if the file is missing or isn't valid TOML, if `[auth].registration` has an unknown value, or if a publish target is incomplete. Both binaries exit with status 1 if `[database].dsn` is empty. An unusable storage, Redis or LDAP setting stops startup with an error in the log.

### A minimal file

```toml
[server]
frontend_url = "https://kribble.example.com"

[security]
jwt_secret     = "<openssl rand -hex 32>"
refresh_secret = "<openssl rand -hex 32>"

[database]
dsn = "postgres://kribble:<db-password>@localhost:5432/kribble?sslmode=disable"

[meili]
host = "http://127.0.0.1:7700"
key  = "<meili-master-key>"

[storage]
backend = "disk"
dir     = "/opt/kribble/data"

[storage.s3]
public_bucket_url = "https://kribble.example.com/files-public"
```

`config.toml.example` in the release contains every section with comments.

## \[server\]

| Key | Default | What it does |
| --- | --- | --- |
| `mode` | `"prod"` | `"prod"` or `"dev"`. Dev mode drops the `Secure` flag from cookies (so plain HTTP works), logs at debug level in console format, also writes logs to `../logs`, and serves the API docs at `/api/v1/docs`. Use `"prod"` on any real server. |
| `port` | `"8080"` | Port `kribble-web` listens on, on all interfaces. Keep it behind your proxy. |
| `frontend_dir` | `"../client/dist"` | Directory of the built web app. Only used by binaries built without the embedded web app; release binaries ignore it. |
| `frontend_url` | `"http://localhost:5173"` | The public address of the server, without a trailing slash. Used for links in emails (verification, password reset, invitations) and for the site addresses that the CLI and MCP report for the built-in target. **Set it** on every server. |
| `trusted_proxies` | `""` | Comma-separated IPs or CIDRs of the reverse proxies whose `X-Forwarded-For` is believed. Empty trusts proxies on this machine and on private networks (`127.0.0.0/8`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7`), which covers a proxy on the same host or in a Docker network. Set it when your proxy reaches Kribble from a public address (Cloudflare, a load balancer). Requests from anywhere else count as their own address, whatever headers they send, so they can't dodge rate limits. |
| `log_request_body` | `false` | Adds request bodies to the access log, capped in size. Sign-in, password and token endpoints are never logged. For debugging only. |
| `log_response_body` | `false` | The same for response bodies. Binary data, HTML, CSS, JS and PDFs are skipped. |

## \[security\]

| Key | Default | What it does |
| --- | --- | --- |
| `jwt_secret` | none | Signs the 15-minute access tokens, the private-site session cookie and the file-download cookie. |
| `refresh_secret` | none | Signs the 7-day refresh tokens. |

Use two different random values of at least 32 characters (`openssl rand -hex 32`). Kribble doesn't check their length or that they were changed from `CHANGE_ME`, so check them yourself. Changing either one signs everyone out. Access tokens (`kb_…`) used by the CLI and CI are not affected, because they are random values stored as hashes in the database.

## \[database\]

| Key | Default | What it does |
| --- | --- | --- |
| `dsn` | none, required | PostgreSQL connection string, for example `postgres://kribble:<password>@localhost:5432/kribble?sslmode=disable`. Use `sslmode=require` or `verify-full` for a database on another host. |
| `auto_migrate` | `true` | Creates and updates the tables when a binary starts. There is no separate migration command, so leave it on unless you apply schema changes some other way. |

Each process opens up to 25 connections.

## \[meili\]

| Key | Default | What it does |
| --- | --- | --- |
| `host` | `""` | Meilisearch URL, for example `http://127.0.0.1:7700` (Docker: `http://meilisearch:7700`). |
| `key` | `""` | The Meilisearch master key, as passed to `--master-key` or `MEILI_MASTER_KEY`. |

Kribble starts even if Meilisearch can't be reached, and sets up the search index in the background once it can (`search: index setup failed; search may be degraded` in the log until then). While Meilisearch is unreachable, notes still save, but full-text search fails and the notes changed meanwhile are missing from search until they are saved again or the index is rebuilt. See [Troubleshooting](../running-a-server/troubleshooting.md).

## \[email\]

| Key | Default | What it does |
| --- | --- | --- |
| `provider` | `""` | `"resend"`, `"smtp"` or `"none"`. When empty, Kribble picks `resend` if `[email.resend].api_key` is set, otherwise `smtp` if `[email.smtp].host` is set, otherwise none. An unknown value, or a provider without its settings, falls back to none and logs an error. |
| `from` | `"noreply@kribble.app"` | Sender address. Mail is sent as `Kribble <from>`. Use an address your mail provider lets you send from. |

With no provider, emails are only written to the log, accounts are created without verification, and password reset and email invitations don't reach anyone. [Authentication](../running-a-server/authentication.md) explains what changes.

### \[email.resend\]

| Key | Default | What it does |
| --- | --- | --- |
| `api_key` | `""` | A Resend API key. The `from` domain must be verified in Resend. |

### \[email.smtp\]

| Key | Default | What it does |
| --- | --- | --- |
| `host` | `""` | SMTP server host name. Required for SMTP. |
| `port` | `"587"` | SMTP port, as a string. |
| `username` | `""` | Login for SMTP AUTH (PLAIN). Leave empty for a relay that needs no login. |
| `password` | `""` | Password for `username`. |
| `encryption` | from the port | `"tls"` (TLS from the start, usual on 465), `"starttls"` (usual on 587) or `"none"`. When empty: port 465 → `tls`, 25 → `none`, anything else → `starttls`. |
| `insecure_skip_verify` | `false` | Accepts any certificate from the SMTP server. For testing only. |

## \[pdfgen\]

| Key | Default | What it does |
| --- | --- | --- |
| `chrome_path` | `""` | Path to the Chrome or Chromium binary used for PDF export. When empty, Kribble uses `CHROME_BIN` if set, otherwise looks for `google-chrome`, `chrome`, `chromium` and `chromium-browser` on `PATH`, the usual `/usr/bin` paths and the macOS application paths. |

Only `kribble-web` exports PDFs.

## \[storage\]

| Key | Default | What it does |
| --- | --- | --- |
| `backend` | `"s3"` | `"disk"` keeps files in a local directory. `"s3"` uses S3-compatible object storage and then requires `[storage.s3]`. |
| `dir` | `""` | Disk backend only, required for it: the root directory. Kribble creates `<dir>/private` (uploads, private sites) and `<dir>/public` (public site assets). It must be writable by the service user (see [Installing](../running-a-server/installing.md) for the systemd sandbox) and shared by `kribble-web` and `kribble-builder`. |

With the disk backend, every file is served through `kribble-web`. It works on one host only, unless the directory is a shared mount.

## \[storage.s3\]

| Key | Default | What it does |
| --- | --- | --- |
| `endpoint` | `""` | S3 API endpoint, for example `https://<account-id>.r2.cloudflarestorage.com` or `https://s3.us-east-1.amazonaws.com`. Required for `s3`. |
| `region` | `"auto"` | `"auto"` suits Cloudflare R2. Use the real region (for example `us-east-1`) for AWS S3 and some MinIO setups. |
| `access_key_id` | `""` | Access key. Required for `s3`. |
| `secret_access_key` | `""` | Secret key. Required for `s3`. |
| `bucket` | `""` | The private bucket: all uploads and private sites. Required for `s3`. |
| `public_bucket` | `""` | The bucket for public sites' assets. It doesn't need public access, because `kribble-web` streams it at `/files-public/`. |
| `public_bucket_url` | `""` | The URL prefix written into public sites for their assets. Set it to `https://<your server>/files-public`. **The disk backend uses it too.** If it points elsewhere (the example file has `http://localhost:8080/files-public`), public sites load without images and icons. |
| `presign_ttl_minutes` | `10` | How long the presigned download links for large files stay valid. |
| `proxy_threshold_bytes` | `15728640` (15 MiB) | Files up to this size are streamed through `kribble-web`. Larger ones are sent as a redirect to a presigned URL, so browsers must be able to reach `endpoint`. |
| `rate_limit_ops_per_sec` | `1000` | Per process: the most write, copy and delete requests sent to S3 per second for user actions. |
| `bg_rate_limit_ops_per_sec` | `200` | Per process: the limit for background work such as site builds, so uploads keep headroom. |

The keys must be allowed to read, write, list and delete objects in both buckets.

## \[builder\]

| Key | Default | What it does |
| --- | --- | --- |
| `workers` | `2` | How many sites `kribble-builder` builds at the same time. |
| `port` | `"8081"` | Port of the builder's health server (`/healthz`, `/readyz`). |

## \[downloads\]

| Key | Default | What it does |
| --- | --- | --- |
| `dir` | `"downloads"` | Directory with the CLI, editor extensions and desktop apps the server hands out, and their `manifest.json`. Relative to the working directory: `/opt/kribble/downloads` with `install.sh`, `/app/downloads` in Docker. A missing directory is not an error. The server then offers no downloads. See [The CLI and desktop downloads](../running-a-server/the-cli-and-desktop-downloads.md). |

## \[redis\]

| Key | Default | What it does |
| --- | --- | --- |
| `url` | `""` | Redis URL, for example `redis://localhost:6379/0` (Docker: `redis://redis:6379/0`). Empty means in-memory caches in each process, which is right for a single `kribble-web`. When set, Redis holds the caches for workspace versions, unread notification counts, published-site access and space access, so several `kribble-web` processes agree. If Redis can't be reached at startup, the binary exits. |

## \[auth\]

| Key | Default | What it does |
| --- | --- | --- |
| `registration` | `"open"` | `"open"`: anyone can register at `/login`. `"invite_only"`: registration needs an invitation to an organisation, and the sign-up form is hidden. LDAP logins still create accounts. Any other value stops startup. |

## \[admin\]

| Key | Default | What it does |
| --- | --- | --- |
| `emails` | `[]` | Accounts with these emails (case-insensitive) are made instance admins each time a binary starts. Promote-only: removing an email doesn't demote anyone. When the list is empty and there is no admin yet, the next account created becomes an admin. |

[Authentication](../running-a-server/authentication.md) covers instance admins in detail.

## \[\[ldap\]\]

Zero or more LDAP or Active Directory sources. Repeat the block for each. Enabled sources are tried in the order they appear. [Authentication](../running-a-server/authentication.md) explains the login flow and has examples.

| Key | Default | What it does |
| --- | --- | --- |
| `name` | `""` | Label for the source. Shown in the admin panel and stored on accounts it creates. |
| `enabled` | `false` | Only enabled sources are used. An enabled source without `url` or `user_search_base` stops startup. |
| `url` | `""` | `ldaps://host:636` or `ldap://host:389`. |
| `bind_dn` | `""` | Service account that searches for users. Empty means an anonymous search. |
| `bind_password` | `""` | The service account's password. Never shown in the admin panel. |
| `user_search_base` | `""` | Base DN searched for users (whole subtree). |
| `user_filter` | `"(\|(uid=%s)(mail=%s))"` | Search filter. Every `%s` is replaced with what the user typed, escaped. It must match exactly one entry. |
| `attr_email` | `"mail"` | Attribute holding the email. Required on the entry. |
| `attr_username` | `"uid"` | Attribute holding the username. Use `"sAMAccountName"` for Active Directory. Required on the entry. |
| `attr_display_name` | `"displayName"` | Attribute for the display name. Falls back to `cn`. |
| `attr_first_name` | `""` | Optional. With `attr_surname`, builds the display name from `display_name_format`. |
| `attr_surname` | `""` | Optional. See `attr_first_name`. |
| `display_name_format` | `"{first} {surname}"` | Template using `{first}` and `{surname}`. Use `"{surname} {first}"` for surname-first names. Only used when `attr_first_name` or `attr_surname` is set. |
| `tls_skip_verify` | `false` | Accepts any certificate on `ldaps://`. For testing only. |
| `timeout_seconds` | `5` | Connect and search timeout. |
| `group_search_base` | `""` | Base DN for the group lookup. Empty turns group sync off for this source. |
| `group_member_attribute` | `"memberUid"` | Group attribute listing members: `memberUid` for posixGroup, `member` for groupOfNames and Active Directory. |
| `user_group_attribute` | `"uid"` | The user's value that appears in the member attribute. `"DN"` uses the user's full DN. |
| `group_membership_filter` | `""` | Optional extra filter combined with the group search, for example `"(\|(cn=devs)(cn=admins))"`. |

### \[\[ldap.team_map\]\]

Repeat under an `[[ldap]]` block. Entries missing any of the three keys are ignored.

| Key | Default | What it does |
| --- | --- | --- |
| `group_dn` | none | DN of an LDAP group (compared case-insensitively). |
| `org` | none | Name of an existing organisation (its URL name, lowercase). |
| `teams` | none | Names of existing teams in that organisation, matched exactly. |

## \[\[publish_targets.local\]\], \[\[publish_targets.ssh\]\], \[\[publish_targets.cloudflare_pages\]\]

Destinations for published sites besides the built-in one. Names must be unique across all three lists. [Publish targets](../running-a-server/publish-targets.md) explains how they behave.

### \[\[publish_targets.local\]\]

| Key | Default | What it does |
| --- | --- | --- |
| `name` | none, required | Name shown in the publish dialog and stored on each site. Don't rename it once sites use it. |
| `base_dir` | none, required | Directory on the Kribble host. Each site goes into a subdirectory of it. |
| `allowed_orgs` | `[]` | Organisation names that may use the target, or `["*"]` for all. Empty means nobody. |

### \[\[publish_targets.ssh\]\]

| Key | Default | What it does |
| --- | --- | --- |
| `name` | none, required | As above. |
| `hostname` | none, required | SSH server. |
| `port` | `22` | SSH port. |
| `username` | none, required | Remote user. |
| `private_key_path` | none, required | Private key file on the Kribble host. |
| `passphrase` | `""` | The key's passphrase, if it has one. |
| `known_hosts_path` | `""` | known_hosts file with the server's host key. Startup doesn't check it, but every publish fails without it. |
| `remote_base_dir` | `""` | Remote directory. Each site goes into a subdirectory of it. |
| `allowed_orgs` | `[]` | As above. |

### \[\[publish_targets.cloudflare_pages\]\]

| Key | Default | What it does |
| --- | --- | --- |
| `name` | none, required | As above. |
| `account_id` | none, required | Cloudflare account ID. |
| `api_token` | none, required | API token with Cloudflare Pages edit permission. |
| `production_branch` | `"main"` | Branch name for the project and its deployments, so every deployment is a production deployment. |
| `project_prefix` | `"kribble"` | Start of each project name: `<prefix>-<name>-<slug>`. Lowercased; other characters become `-`. |
| `allowed_orgs` | `[]` | As above. |
| `api_base_url` | `"https://api.cloudflare.com/client/v4"` | Cloudflare API address. Only change it for testing. |

## Environment variables

### kribble-web and kribble-builder

| Variable | Default | What it does |
| --- | --- | --- |
| `KRIBBLE_CONFIG_PATH` | `config.toml` | Path of the config file. |
| `LOG_LEVEL` | `info` (`debug` in dev mode) | `debug`, `info`, `warn` or `error`. |
| `LOG_FORMAT` | `json` (`console` in dev mode) | Format of the log on standard output. |
| `LOG_DIR` | none (`../logs` in dev mode) | Also write the log as JSON to `<LOG_DIR>/app.log`, rotated. The directory must be writable, so use a path under `/opt/kribble` with the default units. |
| `LOG_MAX_SIZE_MB` | `50` | Rotate `app.log` at this size. |
| `LOG_MAX_BACKUPS` | `10` | Rotated files to keep. |
| `LOG_MAX_AGE_DAYS` | `28` | Delete rotated files older than this. |
| `CHROME_BIN` | none | Chromium path, used when `[pdfgen].chrome_path` is empty. |
| `GIN_MODE` | none | Set to `release` by the systemd units. It quiets the HTTP framework's startup messages. |

Set them in a unit drop-in (`sudo systemctl edit kribble-web`):

```ini
[Service]
Environment=LOG_LEVEL=debug
```

### Installers and Docker Compose

| Variable | Used by | What it does |
| --- | --- | --- |
| `INSTALL_DIR` | `install.sh`, `install-www.sh` | Install location. Default `/opt/kribble` and `/opt/kribble-www`. |
| `KRIBBLE_WEB_BIN`, `KRIBBLE_BUILDER_BIN` | `install.sh` | Paths of the binaries, if they aren't next to the script. |
| `KRIBBLE_PORT`, `BUILDER_PORT` | `install.sh` | The web and builder ports written into a new `config.toml` (an existing one keeps its ports). |
| `KRIBBLE_WWW_PORT` | `install-www.sh` | Port of `kribble-www` (default `8080`). |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | `docker-compose.yml` | Create the database in the `db` container. They must match `[database].dsn`. |
| `MEILI_KEY` | `docker-compose.yml` | Meilisearch master key. Must match `[meili].key`. |
| `KRIBBLE_PORT` | `docker-compose.yml` | Host port for `kribble-web` (default `8080`). |

`kribble-www` reads `KRIBBLE_WWW_ADDR` and `KRIBBLE_WWW_DOWNLOADS`. See [The CLI and desktop downloads](../running-a-server/the-cli-and-desktop-downloads.md).
