Skip to content

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

VariableRequiredDefaultWhat it does
DATABASE_URLyesPostgres connection string. Prefilled in the shipped Compose file; only POSTGRES_PASSWORD is yours to pick.
APP_URLyesThe public URL of the instance. Better Auth’s baseURL, and the webhook endpoint shown in Settings.
SECRET_KEYyesopenssl rand -hex 32. See below.
PORTno3000Port the app binds inside the container.
DATA_DIRno/dataWhere attachments are stored, content-addressed by SHA-256.
REGISTRATION_ENABLEDnofalseSelf-serve registration of a user and their first organization. Invitations and the /setup claim work either way.
LOG_LEVELnoinfoall, trace, debug, info, warn, error, fatal, none — either case.
INSTANCE_MAIL_HOSTyesSMTP host for instance mail.
INSTANCE_MAIL_PORTno587SMTP port for instance mail.
INSTANCE_MAIL_TLSnofalseWhether instance mail uses implicit TLS.
INSTANCE_MAIL_USERnoemptySMTP username for instance mail. Set it together with INSTANCE_MAIL_PASSWORD.
INSTANCE_MAIL_PASSWORDnoemptySMTP password for instance mail. Set it together with INSTANCE_MAIL_USER.
INSTANCE_MAIL_FROMyesThe whole From header for instance mail; a display name is allowed.
TRUSTED_PROXIESnoemptyComma-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_KEY loses 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, From display 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_KEY
  • PADDLE_WEBHOOK_SECRET
  • PADDLE_PRICE_STARTER_MONTHLY, PADDLE_PRICE_STARTER_YEARLY, and PADDLE_PRICE_STARTER_TRIAL_MONTHLY
  • PADDLE_PRICE_GROWTH_MONTHLY, PADDLE_PRICE_GROWTH_YEARLY
  • PADDLE_PRICE_PRO_MONTHLY, PADDLE_PRICE_PRO_YEARLY
  • PADDLE_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.