Configuration reference
If the process cannot start without it, it is an environment variable. Everything else is a setting in the app. That is the whole rule.
No config file, no *_FILE Docker-secret variants, and no env fallback for anything that
lives in Settings — supporting both means owning a precedence bug for the rest of the
project’s life.
Environment variables
| Variable | Required | Default | What it does |
|---|---|---|---|
DATABASE_URL | yes | — | Postgres connection string. Prefilled in the shipped Compose file; only POSTGRES_PASSWORD is yours to pick. |
APP_URL | yes | — | The public URL of the instance. Better Auth’s baseURL, and the webhook endpoint shown in Settings. |
SECRET_KEY | yes | — | openssl rand -hex 32. See below. |
PORT | no | 3000 | Port the app binds inside the container. |
DATA_DIR | no | /data | Where attachments are stored, content-addressed by SHA-256. |
REGISTRATION_ENABLED | no | false | Self-serve registration of a user and their first organization. Invitations and the /setup claim work either way. |
LOG_LEVEL | no | info | all, trace, debug, info, warn, error, fatal, none — either case. |
INSTANCE_MAIL_HOST | yes | — | SMTP host for instance mail. |
INSTANCE_MAIL_PORT | no | 587 | SMTP port for instance mail. |
INSTANCE_MAIL_TLS | no | false | Whether instance mail uses implicit TLS. |
INSTANCE_MAIL_USER | no | empty | SMTP username for instance mail. Set it together with INSTANCE_MAIL_PASSWORD. |
INSTANCE_MAIL_PASSWORD | no | empty | SMTP password for instance mail. Set it together with INSTANCE_MAIL_USER. |
INSTANCE_MAIL_FROM | yes | — | The whole From header for instance mail; a display name is allowed. |
TRUSTED_PROXIES | no | empty | Comma-separated CIDR ranges of your reverse proxies. See Reverse proxy. |
APP_URL, SECRET_KEY, INSTANCE_MAIL_HOST and INSTANCE_MAIL_FROM deliberately have no
defaults. A guessed base URL breaks every deep link and the auth cookie silently, which is
worse than refusing to boot.
Instance mail
Instance mail is Kiku talking to its own agents, admins and invitees: password resets, invitations, agent notifications once shipped, and operational alerts. Desk mail is the desk talking to customers through an organization’s support mailbox. They are separate senders; desk mail is configured in Settings → Email.
The simplest correct setup is to reuse the support mailbox’s own SMTP credentials for instance
mail. INSTANCE_MAIL_FROM is the whole From header, so Kiku <instance@example.com> is valid
and keeps its display name.
SECRET_KEY
One secret, four keys derived from it by labelled HKDF: auth for sessions, contact-form
for the tokens a Contact form page must send back with a submission, reply-token for the
signed reply addresses that thread email, and settings for AES-256-GCM encryption of
stored mailbox credentials, AI keys and Integration tokens. Nothing uses the raw value, so a
leak of one derived key is not a leak of the others.
Two consequences:
- Losing
SECRET_KEYloses encrypted credentials. Stored mailbox credentials, AI keys and Integration tokens cannot be decrypted without it, and the app says so rather than failing obscurely. Re-entering them is the fix — but that is why the key is part of a backup. See Upgrade and backup. - Changing it logs everyone out and invalidates every reply token already in the wild, so in-flight customer replies fall back to the lower threading layers. It also invalidates the token of every Contact form page open at that moment, and a visitor who sends from one is told the message was sent while nothing is stored.
GitHub Enterprise hosts
An Enterprise host accepts a bare hostname, an http(s) scheme, and trailing slashes. Kiku
stores the lowercase host[:port] only — no scheme or path — and rejects paths, credentials,
queries, and fragments.
Everything else is Settings
Configured in the app, stored in Postgres, credentials encrypted at rest:
- Settings → Email — support address, inbound mode (IMAP or webhook), IMAP host / port /
user / password / TLS, SMTP the same, poll interval,
Fromdisplay name, the provider decoder shim for the webhook path, the app-generated webhook secret, the subject-token toggle (off by default), and the auto-acknowledgement toggle (on by default — its limits are not configurable, see Limits). See Email setup. - Settings → Ticket types and Settings → Companies.
- Settings → AI — base URL, API key, model. Off unless configured; read Limits before you point it at a hosted provider.
- Settings → Integrations — one Goodday token and one GitHub PAT per organization. See Integrations.
Saved changes take effect without a restart: the IMAP poller re-reads settings at the top of each cycle, so a change lands within one poll interval, and the SMTP transport is built per send.
The cost of having no env fallback is real and accepted: a GitOps operator cannot declare their mailbox in a manifest. It is Settings or nothing.
Not configurable at all
Deliberately constants, not knobs — guards and internals nobody tunes:
- the 25 MB inbound cap
- the Contact form limit per client IP (see Limits)
- the polled IMAP folder (
INBOX) - the
reply+prefix and its separator - the attachment storage layout
They become settings when a real operator asks for one, not before.
Billing
Leave every PADDLE_* value unset to disable billing. To enable it, set all of:
PADDLE_API_KEYPADDLE_WEBHOOK_SECRETPADDLE_PRICE_STARTER_MONTHLY,PADDLE_PRICE_STARTER_YEARLY, andPADDLE_PRICE_STARTER_TRIAL_MONTHLYPADDLE_PRICE_GROWTH_MONTHLY,PADDLE_PRICE_GROWTH_YEARLYPADDLE_PRICE_PRO_MONTHLY,PADDLE_PRICE_PRO_YEARLYPADDLE_PRICE_SCALE_MONTHLY,PADDLE_PRICE_SCALE_YEARLY
Set Paddle’s notification URL to <APP_URL>/api/billing/paddle/webhook. Each subscription
must carry Kiku’s opaque organization id as custom_data.organizationId.