Server configuration reference
Last reviewed: 2026-10-06.
Every setting the Kotiko server reads, the files it keeps, and the commands that manage
them. The server is configured only through environment variables; run.sh reads them
from server/.env. A test (server/test/kotiko/docs_test.exs) fails when the server
learns a setting this page doesn't describe.
- How settings are read
- Settings: network, API token, model, Telegram, voice notes, data and logs
- Secrets in files
- Other variables
- Files in the data folder
mix kotiko.token
How settings are read
Section titled “How settings are read”- Copy the example and keep it private:
cp server/.env.example server/.env && chmod 600 server/.env.run.shmakes.envprivate itself if other users can read it. run.shloads.envwith the shell (set -a; . ./.env), so quote a value that holds$, spaces or#with single quotes:LLM_API_KEY='abc$def'. Variables already in the environment work too;.envwins when both set one.- Values are trimmed. An empty value counts as not set.
- The server reads settings only when it starts. Restart it after a change
(
systemctl --user restart kotiko, or stop./run.shand start it again). - When a setting is wrong, the server doesn't start. It prints one message naming every
setting to fix and how, and exits with status 78. The systemd unit from
install-service.shdoesn't retry on status 78, so fix.envand restart by hand. A message about a secret (API_TOKEN,LLM_API_KEY,TELEGRAM_BOT_TOKEN,TRANSCRIBE_API_KEY) never repeats its value, whether it came from.envor from a file. - Secrets can live in their own files instead of
.env: setLLM_API_KEY_FILEto a file's path, and so on for each secret. See Secrets in files. - Warnings don't stop the server: a missing OpenRouter key, an old setting name, and a
KOTIKO_name the server doesn't know ("did you mean KOTIKO_DATA_DIR?"). - The source of truth is
Kotiko.Config(server/lib/kotiko/config.ex);server/config/runtime.exspasses these variables to it.
Settings
Section titled “Settings”The TCP port the server listens on.
- Default:
4747. Allowed: a whole number from 1 to 65535. - Wrong:
PORT=abcstops the server ("isn't a port number").
The address the server listens on.
- Default:
127.0.0.1, this computer only. - Allowed: an IPv4 or IPv6 address (
100.101.102.103,0.0.0.0,::1) or a host name that resolves to one (localhost). - Example: your Tailscale address, to use the extension on another computer.
- At start, the server logs who can reach it. For a local network address,
0.0.0.0or a public address, that line is a warning: the server speaks plain HTTP, so anyone on the way can read the token. Use Tailscale or put HTTPS in front (README, "Browse on another machine"). Tailscale addresses (100.64.0.0/10) get an informational line. - Wrong: a name that doesn't resolve stops the server.
ALLOWED_HOSTS
Section titled “ALLOWED_HOSTS”Extra host names the server answers to, comma-separated, besides localhost, IP addresses
and this computer's own name (and <name>.local). Requests for any other name get 421;
this stops web pages from reaching the server through DNS rebinding.
- Default: none.
- Example:
ALLOWED_HOSTS=laptop.tail1234.ts.net,words.example.com. - Names are lowercased and a trailing dot is dropped. Write just the name, without
http://or a port. *turns the check off; use it only behind a proxy that checks host names itself.- Wrong: an entry that isn't a host name stops the server.
PUBLIC_URL
Section titled “PUBLIC_URL”The address the extension should use, when it isn't http://<BIND>:<PORT> (for example
behind a reverse proxy). Used only in the pairing string from
mix kotiko.token.
- Default: none. Allowed: an
http://orhttps://address; a trailing/is dropped. - Wrong: anything else stops the server ("isn't a web address").
API_TOKEN
Section titled “API_TOKEN”The token the extension sends as Authorization: Bearer <token> (see the
HTTP API). A secret.
- Default: none. Then the server uses the token saved in
<data folder>/api-token, and on the first start makes one there: 32 random bytes from the operating system's secure random source, written as 43 URL-safe characters, in a file only you can read (mode 0600). - Allowed: at least 24 characters. Example: the output of
openssl rand -hex 24. - Wrong: a shorter token stops the server (the message gives its length, not its value).
- To replace the saved token:
mix kotiko.token --rotate, then restart. WithAPI_TOKENset, edit.envinstead; withAPI_TOKEN_FILE, replace the file's contents. - Or from a file:
API_TOKEN_FILE.
LLM_API_KEY
Section titled “LLM_API_KEY”The key for the model API that works out which word you mean. A secret.
- Default: none. With OpenRouter (the default
LLM_URL), the server starts with a warning and adds answer503 lookup_not_set_upuntil you set it. A local model such as Ollama needs no key. - Example: a key from https://openrouter.ai/keys.
- Sent only to
LLM_URL. - Or from a file:
LLM_API_KEY_FILE.
LLM_URL
Section titled “LLM_URL”The model API: any OpenAI-compatible API's base URL.
- Default:
https://openrouter.ai/api/v1. - Example:
http://localhost:11434/v1for a local Ollama. - Allowed: an
http://orhttps://address. Wrong: anything else stops the server. - Your typed text goes to this address (README, "A different model").
LLM_MODEL
Section titled “LLM_MODEL”Which models to ask, comma-separated, in order: when one is busy or finds nothing, the next one gets the word.
- Default: empty. With OpenRouter, the server then reads OpenRouter's list of free models
once a day and asks the best ones first (
spec/models.json); until the first read it uses the list shipped inspec/models.json. - Example:
LLM_MODEL=llama3.2for Ollama. - Wrong: empty while
LLM_URLisn't OpenRouter stops the server ("Set LLM_MODEL to a model it has").
TELEGRAM_BOT_TOKEN
Section titled “TELEGRAM_BOT_TOKEN”The token of your own Telegram bot, from @BotFather, to add words from your phone. A secret.
- Default: none; the bot is off.
- Allowed: the shape @BotFather gives, digits, a colon, then at least 30 letters, digits,
_or-. Wrong: anything else stops the server. - With a token and no
ALLOWED_TELEGRAM_IDS, the bot answers every message only with the sender's Telegram ID and how to allow it. - Or from a file:
TELEGRAM_BOT_TOKEN_FILE.
ALLOWED_TELEGRAM_IDS
Section titled “ALLOWED_TELEGRAM_IDS”The Telegram user IDs the bot works for, comma-separated. Everyone else is ignored.
- Default: none (see above).
- Example:
ALLOWED_TELEGRAM_IDS=123456789,987654321. - Allowed: whole numbers (a leading
-is accepted). Wrong: anything else stops the server.
TRANSCRIBE_URL
Section titled “TRANSCRIBE_URL”A speech-to-text endpoint for Telegram voice notes: a local whisper.cpp server
(http://localhost:8178/inference) or any OpenAI-compatible
/v1/audio/transcriptions URL. The server sends the voice note there as a multipart
upload.
- Default: none; voice notes aren't transcribed (typing still works).
- Allowed: an
http://orhttps://address. Wrong: anything else stops the server.
TRANSCRIBE_MODEL
Section titled “TRANSCRIBE_MODEL”The model name sent with each voice note.
- Default:
whisper-1. Example:large-v3.
TRANSCRIBE_API_KEY
Section titled “TRANSCRIBE_API_KEY”Sent as Authorization: Bearer <key> to TRANSCRIBE_URL, for hosted services. A secret.
- Default: none (a local whisper.cpp server needs none).
- Or from a file:
TRANSCRIBE_API_KEY_FILE.
KOTIKO_DATA_DIR
Section titled “KOTIKO_DATA_DIR”The folder that holds your words and the server's files (see Files in the data folder).
- Default:
$XDG_DATA_HOME/kotiko, or~/.local/share/kotikowhenXDG_DATA_HOMEisn't set (or isn't an absolute path, which the XDG Base Directory spec says to ignore). The server creates it if it's missing. - Words already in
~/.local/share/kotikostay there: while$XDG_DATA_HOME/kotikohas nokotiko.dband~/.local/share/kotikohas one, the server keeps using~/.local/share/kotiko. To move them, stop the server and move the folder. - Wrong: a folder the server can't create or write to stops the server.
- The name it had before the rename still works for now, with a warning (see the README's section on updating from the old name).
LOG_LEVEL
Section titled “LOG_LEVEL”How much the server logs: debug, info, warning or error (warn works too).
- Default:
info. Wrong: anything else stops the server. - Keys and tokens are kept out of the logs at every level. The words you look up are not
logged unless
LOG_LOOKUPSis on.
LOG_LOOKUPS
Section titled “LOG_LOOKUPS”true to log what you look up and the model's answer, at debug level (so also set
LOG_LEVEL=debug). For troubleshooting a model.
- Default:
false. Allowed:true,yes,on,1,false,no,off,0. - Wrong: anything else stops the server.
KOTIKO_LOG_SQL
Section titled “KOTIKO_LOG_SQL”true to log every database query at info level. For development only: queries carry your
words.
- Default:
false. Allowed: as forLOG_LOOKUPS. - The name before the rename still works for now, with a warning.
KOTIKO_WIKTIONARY
Section titled “KOTIKO_WIKTIONARY”Pronunciations from Wiktionary. For a language with word stress, the server reads how a new
word is said from its English Wiktionary page, sending only the word to
en.wiktionary.org.
- Default: on.
false(orno,off,0) sends nothing there and keeps the model's pronunciation. - Wrong: a value that isn't true or false stops the server.
Secrets in files
Section titled “Secrets in files”Each secret can be given as the path of a file that holds it, instead of the value itself:
NAME_FILE=/path/to/file in place of NAME=value. This is the convention Docker secrets
use, and it works with systemd credentials too. Your keys then stay out of .env, and
replacing one means replacing its file and restarting the server; nothing is rebuilt and
.env doesn't change.
install -m 600 /dev/null ~/.config/kotiko-llm-key # an empty file only you can read$EDITOR ~/.config/kotiko-llm-key # paste the keyecho "LLM_API_KEY_FILE=$HOME/.config/kotiko-llm-key" >> server/.env- The file holds only the value. One line ending at its end is dropped; anything else is kept as written, so don't add spaces or quotes.
- The value is then checked like one set directly (an
API_TOKENof at least 24 characters, a bot token's shape). - The server stops with a message (status 78) when both
NAMEandNAME_FILEare set, or the file is missing, unreadable, not a regular file, empty, longer than one line or over 64 KiB. The message names the setting and the path, never the contents. - A file that other users of the computer can read or change gets a warning at start:
chmod 600it. - The file is read once, when the server starts. A relative path is relative to
server/. mix kotiko.tokenreadsAPI_TOKEN_FILEtoo.
With systemd credentials, for example, a drop-in for the service
(systemctl --user edit kotiko) can hand the server a key without it ever being in .env:
[Service]LoadCredential=llm_api_key:/home/you/.config/kotiko-llm-keyEnvironment=LLM_API_KEY_FILE=%d/llm_api_keyAPI_TOKEN_FILE
Section titled “API_TOKEN_FILE”A file holding API_TOKEN.
LLM_API_KEY_FILE
Section titled “LLM_API_KEY_FILE”A file holding LLM_API_KEY. Example: /run/secrets/llm_api_key with
Docker secrets.
TELEGRAM_BOT_TOKEN_FILE
Section titled “TELEGRAM_BOT_TOKEN_FILE”A file holding TELEGRAM_BOT_TOKEN.
TRANSCRIBE_API_KEY_FILE
Section titled “TRANSCRIBE_API_KEY_FILE”A file holding TRANSCRIBE_API_KEY.
Other variables
Section titled “Other variables”These are read by the scripts and for the default folders, not by the server's settings check.
| Variable | Read by | Meaning |
|---|---|---|
XDG_DATA_HOME |
the server, install-service.sh |
Where the default data folder goes: $XDG_DATA_HOME/kotiko. install-service.sh passes the value it sees to the service, so the service and ./run.sh use the same folder |
XDG_CONFIG_HOME |
install-service.sh |
Where the service file goes: $XDG_CONFIG_HOME/systemd/user/kotiko.service; default ~/.config. A service file already in ~/.config/systemd/user stays there |
MIX_ENV |
run.sh |
Build environment; default prod. Set it in your shell or .env |
ERL_CRASH_DUMP_SECONDS |
run.sh |
Default 0: no crash dump file, because one would hold the server's memory, keys included |
INSTALL_WAIT_SECONDS |
install-service.sh |
How long to wait for /health after installing; default 20 |
Files in the data folder
Section titled “Files in the data folder”| File | What it is | Permissions |
|---|---|---|
kotiko.db (with kotiko.db-wal and kotiko.db-shm while running) |
Your words, kept responses for repeated adds (24 hours), cached lookups (30 days), and the background jobs' state. SQLite | Created with your umask: usually readable by other users of the computer unless your umask is 077. The default folder is inside your home folder. To keep it private: chmod 700 the data folder (usually ~/.local/share/kotiko) |
api-token |
The generated API token, when API_TOKEN isn't set |
0600, written so no other user can read it at any moment |
models-cache.json |
OpenRouter's model list as last read, used when the server starts offline | Your umask |
backups/kotiko-pre-<version>-<time>.db |
A copy of the database made before an update changes it; the newest five are kept | Folder 0700, files 0600 |
To go back to a backup, see the README, "Updating". To start over, stop the server and
delete kotiko.db.
mix kotiko.token
Section titled “mix kotiko.token”Run in server/. Reads the settings the way run.sh does (the environment, then .env
in the current folder).
mix kotiko.token # print the API token and a pairing stringmix kotiko.token --rotate # make and save a new token (not when API_TOKEN is set)mix kotiko.token --env-file PATH # read another .env fileThe pairing string carries the server address and the token in one value for the
extension's connection screen: kotiko-pair:1: followed by base64url-encoded JSON
{"url": ..., "token": ...}. Treat it like the token. After --rotate, restart the server
and paste the new token or pairing string into the extension.
