Configuration
Configuration lives in .env next to docker-compose.yml, with one exception: SMTP is set in the PocketBase admin UI, not in the file. The complete variable list with defaults is in Environment variables; this page covers the decisions rather than the table.
After any change:
docker compose up -d --buildAI
AI is off by default and the app is fully usable that way. Switch it on with KB_AI_PROVIDER:
| Provider | What it means |
|---|---|
none | No AI. Nothing ever leaves your server. This is the default. |
openai | Any OpenAI-compatible API. OpenAI, OpenRouter, Groq, Mistral, Together — or your own vLLM, LM Studio, llama.cpp or liteLLM proxy. Address, key, model name. Dictation works through the same service. |
ollama | Your own local model. Install Ollama, ollama pull gpt-oss:20b, then set KB_AI_URL=http://IP:11434 and KB_AI_MODEL=gpt-oss:20b. Everything stays inside your network. Basic prompts; voice transcription is not included. |
api | A remote AI service compatible with the killBottleneck API — an address and token from your provider. No GPU of your own and no maintenance. |
custom | Your own endpoint honouring the same API contract — the contract is written down here. |
KB_AI_PROVIDER=ollama
KB_AI_URL=http://192.168.1.10:11434
KB_AI_MODEL=gpt-oss:20bConnecting a provider with an API key (openai)
Almost every provider today speaks the OpenAI shape, so one switch covers them all. You need three things: the base address (it usually ends in /v1), your API key, and the exact model name your provider uses.
KB_AI_PROVIDER=openai
KB_AI_URL=https://openrouter.ai/api/v1
KB_AI_TOKEN=sk-...
KB_AI_MODEL=openai/gpt-4o-mini| Service | KB_AI_URL | Example KB_AI_MODEL |
|---|---|---|
| OpenAI | https://api.openai.com/v1 | gpt-4o-mini |
| OpenRouter | https://openrouter.ai/api/v1 | openai/gpt-4o-mini |
| Groq | https://api.groq.com/openai/v1 | (name from your provider) |
| vLLM / LM Studio / llama.cpp | http://IP:PORT/v1 | (the name the server reports) |
Write the address down to the version
The address must point at the root of the OpenAI interface. If your service sits on a sub-path (a liteLLM proxy behind https://company.com/llm, say), write it out in full, including the version — https://company.com/llm/v1. From a bare address we cannot tell whether the service lives at the root and wants /v1 appended (that is how OpenAI does it) or sits exactly there and wants nothing appended, so we append /v1 whenever the path carries no version. An address that already contains a version anywhere in its path (…/v1beta/openai) is left alone. Got it wrong? Test connection tells you at once — you will not find out from silence.
You can also set it up in the app instead of .env — Administration → AI features → OpenAI-compatible — and press Test connection, which tells you straight away whether the key works and whether the model name exists.
Which model to pick — measured, not guessed
Measured on 20 Aug 2026 by running all six AI features of killBottleneck (map from a goal, clarifying questions, sub-steps, map from text, chat over a map, morning summary) through the product itself, in Czech. Price is what those six calls actually cost.
| Model | Passed | Map from a goal | Six features cost |
|---|---|---|---|
gpt-4o-mini ⭐ | 6/6 | 11.2 s, 8 nodes | $0.0011 |
gpt-4.1-mini | 6/6 | 6.4 s, 7 nodes | $0.0025 |
gpt-4.1-nano | 6/6 | 2.8 s, 7 nodes | $0.0005 |
gpt-5-mini | 6/6 | 24.5 s, 7 nodes | $0.0120 |
o4-mini | 6/6 | 16.8 s, 7 nodes | $0.0329 |
gpt-5-nano | 3/6 ❌ | 26.2 s, 7 nodes | $0.0044 |
gpt-4o-mini is the recommended default — it is the one we clicked through end to end, from "Suggest with AI" to a saved map and AI-generated sub-steps. gpt-4.1-nano is cheaper and quicker and passed everything too; gpt-4.1-mini sits in between.
⚠️ Avoid reasoning models (gpt-5-*, o4-*). They are 2–4× slower and 5–30× more expensive, because you also pay for the thinking you never see — o4-mini spent 5,824 of its 7,082 output tokens on reasoning. And gpt-5-nano loses three of the six features outright: it burns the whole budget on thinking and returns nothing. killBottleneck tells you that instead of silently doing nothing, but it is not a model to build on.
The quickest check for any other model is Test connection followed by Suggest a plan from a goal: either a tree appears, or you get an error saying what went wrong.
The key and the money are yours
The key is stored on your server and is never handed back to the browser. Requests are billed by your provider, not by us — killBottleneck sends nothing anywhere on its own.
Dictation goes to the same service (/audio/transcriptions, the whisper-1 model by default — change it with KB_AI_TRANSCRIBE_MODEL). If you would rather run transcription somewhere else, point KB_AI_TRANSCRIBE_URL at it and that address wins.
On a nine-second Czech sentence both whisper-1 (1.9 s) and gpt-4o-mini-transcribe (0.7 s) transcribed every word correctly, diacritics included. The second one is also cheaper ($0.003 vs $0.006 per minute) — but that is one sample, so the default stays whisper-1.
⚠️ "OpenAI-compatible" means the chat, not necessarily transcription. OpenAI has /audio/transcriptions; OpenRouter, vLLM and LM Studio usually do not. killBottleneck therefore asks the service for its model list and only offers dictation when it finds a transcription model there — a microphone that always fails is worse than no microphone. If your service names its model differently (Groq uses whisper-large-v3), set it in Administration → AI features or via KB_AI_TRANSCRIBE_MODEL.
A cap on spending
With your own API key every AI call costs you money, so one person can run at most 60 AI operations per hour and 20 dictations per hour by default (KB_AI_MAX_PER_HOUR, KB_AI_MAX_TRANSCRIBE_PER_HOUR). Without a cap a single account — or a stuck browser tab — could burn your credit in a loop. Above the cap the app says so plainly; nothing is lost. The cap applies only to openai: a local Ollama costs nothing and the AI service via API has its own quota.
Which model to pick
gpt-oss:20b is the verified one — killBottleneck is tested against it. Plan for what it needs:
| Model size | ~14 GB |
| Sensible operation | a GPU with 16 GB of memory or more |
| Answer time | seconds to tens of seconds (~23 s measured for suggesting questions) |
We recommend only this one, because it is the only one we have tested. You are free to try another — Ollama offers plenty — but that is at your own risk. It is not about how "smart" the model is: killBottleneck needs a machine-readable answer (JSON), and smaller or so-called reasoning models sometimes return it truncated — the feature then silently does nothing.
Of the ones we measured, gemma3:12b (~8 GB) held up and is usable on a 12 GB card, just slower. Another model of the same size returned a broken answer on the harder task — which is why "at your own risk" is meant literally. The quickest way to check a new model is to try Suggest a plan from a goal: either a tree appears, or nothing happens.
Without a GPU the model runs on the CPU and answers take minutes; that is not enough to work with.
killBottleneck talks to Ollama directly (/api/chat) — nothing sits in between and nothing leaves your network.
Where your data goes
With any provider other than none, map data is sent to the endpoint you configured. With ollama pointing at a machine you own, that is still your network. With api or custom it is whatever you pointed it at. There is no third option hidden anywhere.
A separate model just for daily summaries
The one-line encouragement in My day is short formatted text — a smaller, faster model is plenty. KB_SUMMARY_PROVIDER / _URL / _MODEL / _TOKEN override the general AI config for that one job. Left empty, the general configuration is used.
The summary is generated in the morning at KB_SUMMARY_HOUR (default 6) only for accounts that signed in within KB_SUMMARY_ACTIVE_DAYS days (default 14, 0 = everyone); everyone else gets theirs when they open the app.
What the AI is not allowed to do
The AI never enumerates your task lists — those are computed from your data and clickable. Task names are sanitised before they enter the prompt. Turning AI off costs you exactly one line in My day; the panel itself keeps working.
Registration and the registration key
The first account to register becomes the administrator. After that, either people register themselves, or an administrator invites them from Administration.
Set KB_SETUP_CODE and every registration requires that key. Hand it to the people you want to let in. An invitation from an administrator does not need the key.
Reachable from the internet?
Set KB_SETUP_CODE before you expose the instance. Empty means open registration.
Sign in with Google
Optional — users then sign in with Google instead of e-mail and password.
- In the Google Cloud Console: APIs & Services → Credentials → Create credentials → OAuth client ID → Web application.
- Under Authorized redirect URIs add:
https://YOUR-DOMAIN/api/oauth2-redirect - Put the values in
.env:bashKB_GOOGLE_CLIENT_ID=…apps.googleusercontent.com KB_GOOGLE_CLIENT_SECRET=… docker compose up -d --build— the button appears by itself.
Empty variables = Google sign-in is off and the button is not shown.
E-mail (SMTP)
SMTP is configured in the PocketBase admin UI, not in .env:
http://SERVER-IP:8090/_/ → Settings → Mail settings
The superuser account is created on first start; the link is in docker compose logs.
What changes if you skip SMTP
E-mail is off by default and the instance is fully usable that way. Only these differ:
| With SMTP | Without SMTP | |
|---|---|---|
| Inviting a member | e-mailed with a link | the admin is shown a temporary password to hand over |
| "Forgot your password?" | self-service, link arrives by e-mail | not offered — the admin resets it (see below) |
| Reminders and daily digest | delivered per each person's preferences | in-app bell only |
The password-reset link is hidden on purpose without SMTP: the server would confirm the request, no message would arrive, and the person would wait for mail that does not exist.
Forgotten password without SMTP
For a team member, the admin resets it: Organisation settings → key icon next to the person → confirm. A new temporary password appears to hand over; the old password stops working immediately and the person is signed out everywhere. They are told in the bell that someone reset it.
For the admin, nobody can do this — there is no one above them (and the app does not offer resetting your own password either). Here the two versions differ:
- Hosted version (killbottleneck.com): e-mail always works, so the admin uses the normal "Forgot your password?" on the sign-in page. Nothing else to handle.
- Your own server without SMTP: the route goes through the PocketBase console, which you have access to as the operator:
http://SERVER-IP:8090/_/→ sign in as superuser- users collection → find your account → Change password
No superuser yet? Create one on the server (service name comes from docker-compose.yml):
docker compose exec killbottleneck /app/pocketbase superuser upsert you@example.com 'YourStrongPassword'Watch out
The console on :8090/_/ runs over plain HTTP — open it only from your local network or through an SSH tunnel, never over the internet. A password typed on the command line also stays in your shell history and is briefly visible to other users of the server; change it in the console afterwards and delete the history entry.
Recommendation
Create a second account with the admin role and keep its password in a password manager. The PocketBase route works, but it is extra work in the middle of a day when nobody can sign in.
Notification e-mails are off by default and controlled by each user's preferences. If e-mails were already reaching your users before an upgrade and you want to keep them, KB_NOTIFY_EMAIL_DEFAULT=1.
Time zone and recurring templates
TZ applies to the whole instance, not per user. It decides which zone "every Monday" and "at 5 in the morning" are counted in. A team spread across zones gets the server's zone.
If the server was powered off during KB_AUTO_HOUR, recurring projects are created at the next later hour the same day — they are not silently skipped.
Attachment limits
KB_FILES_MB caps uploaded files across the whole instance:
- empty — no limit; the self-hosting default, it is your disk
- a number — cap in MB (
5000= 5 GB) 0— uploading disabled; attachments can still be added as links, which take no space and always point at the current version of the file
Why 0 is the default on our hosted version
It is not about saving disk space, it is deliberate: we do not store other people's files. They never enter our backups, they do not turn into "we process customer documents" in a contract, and they stay where they already are. On your own server it is your disk and your call.
Automations
Only relevant if you drive goals from an automation. KB_PUBLIC_URL is the address the agent should call back on — see Environment variables → Automations for why leaving it empty can make a remote agent call itself, and why KB_ALLOW_PRIVATE_WEBHOOKS is off by default.
Instance mode
Leave KB_HOSTED empty on your own server. It exists for instances that run at a provider alongside other customers, where the instance administrator must not be able to point the AI service into a private network and probe the neighbours.

