Skip to content

Limits

What Kiku deliberately does not do. Every entry here is a decision, not a gap waiting to be filled — read it before you build a workflow on top of an assumption.

One instance. Never two.

Do not scale the app to two replicas. Realtime updates are an in-process pub/sub feeding Server-Sent Events, so with two containers each agent’s browser is connected to exactly one of them and sees only the events that happened to land there. Half your inbox stops updating, and nothing errors.

There is no clustering flag to get wrong; the only way to reach this state is to add deploy.replicas or run the image twice against one database, so don’t. A single container handles the volume a shared support inbox produces with room to spare.

The upgrade path, if it is ever needed, is Postgres LISTEN/NOTIFY — no new service.

No antivirus scanning

Attachments are stored and served without being scanned, and there is no ClamAV service in the Compose file.

The server never executes attachments. The exposures that actually exist are handled without a scanner: files are served with Content-Disposition: attachment, X-Content-Type-Options: nosniff and a Content-Type we choose rather than one the sender claims, and email HTML is sanitised with remote resources proxied. A required ClamAV service is 1–2 GB of RAM — heavier than the app itself — for a boundary that the agent’s own endpoint protection already is.

The attachment volume is a plain directory. If you have a compliance requirement, run clamscan against it on a cron.

Agents cannot reply by email

Agent notification mail has not shipped yet. When it does it will be read-only, with a deep link into the app: replying to a Kiku notification will not post a message.

This is not laziness. Matching From: against a known agent would let anyone who learns an agent’s address post as them, into a product where internal notes sit inline with customer messages. And every message in Kiku is explicitly public or internal, never inferred — an emailed reply carries no such signal, so it would need a magic body command, where forgetting the marker mails an internal note to the customer. That is the worst bug this product could ship.

Mobile is answered by the UI being responsive.

Microsoft 365 and Google Workspace mailboxes

Kiku polls IMAP with a username and password, and ships no OAuth app for either. A mailbox that requires OAuth cannot be connected directly — forward it into the subdomain address instead. See Coming from Microsoft 365.

The auto-acknowledgement has no knobs but one

You can turn the automatic reply to new tickets off in Settings → Email. Everything else about it is fixed, and none of it is exposed:

  • One acknowledgement per ticket, on creation only. Follow-ups are never acknowledged.
  • Never sent to a machine. Mail declaring Auto-Submitted, Precedence: bulk/list/junk, a List-Id or List-Unsubscribe header, a null return path, a Message-ID in our own shape, a delivery report, or an upstream spam verdict (X-Spam-Status: Yes or Microsoft SCL 5-9) gets a ticket but no reply.
  • Rate limits, hardcoded: 4 acknowledgements an hour and 10 a day per sender address, 30 an hour per sender domain, 200 an hour across the instance. Over any of them, the reply is withheld and the ticket is unaffected.
  • 60 inbound messages an hour from one address. Past that, further mail from that address is quarantined rather than ticketed — never dropped; it is still there to read.

These numbers are what stops your instance amplifying a mail loop or a mail bomb at someone else. A setting that could raise them would have exactly one use.

The acknowledgement text is likewise fixed, and English only.

The Contact form has one limit

A client IP can send 30 Contact form submissions an hour and 100 a day, counted across every form and organization on the instance. Past either number the form tells the visitor to email the support address instead, and Kiku stores nothing. IPv6 clients count per /64. The limit is hardcoded, and there is no per-form or per-organization limit.

The limit keys on the client address, so behind a reverse proxy it needs TRUSTED_PROXIES. Without it, every visitor shares the proxy’s address and one limit.

It stops one network from flooding a form. It does not protect your quota: a submission opens a billable ticket like an email does, and someone rotating addresses can spend quota through the form with nothing else to stop them.

AI: where your ticket content goes

AI ticket-type classification is off unless you configure it in Settings → AI. There is no default provider and no key ships with the app.

It runs once, when a ticket is opened, after the acknowledgement has been sent — never on a later message, and there is no “classify” button. It sets a type only on a ticket that has none, so it can never overwrite an agent, and a model that times out, errors or answers with something that is not one of your type names leaves the ticket in Untriaged. There is no retry: an untriaged ticket is a normal outcome, not a failure to recover from.

With a hosted provider configured, the subject and the first message of every new ticket are sent to that provider. Only new tickets, only the first message, never the rest of the thread and never internal notes — but that is customer email leaving your server, and the statement is exactly that plain because a half-working scrubber would sell a privacy guarantee the code does not keep.

If that is unacceptable: point the base URL at a local model — Ollama, vLLM, anything speaking the OpenAI-compatible API — or leave the key unset and the feature stays off. Nothing else in Kiku depends on it.

Also true, and by design

  • No downgrade path. See Upgrade and backup.
  • No env fallback for anything in Settings. Mailbox configuration cannot be declared in a manifest. See Configuration reference.
  • Four ticket statuses, fixed — Open, Blocked, Waiting on customer, Closed. Not configurable.
  • English only. No i18n in v1, including the auto-acknowledgement text.
  • /setup is an open claim. Between first boot and claiming the admin account, anyone who can reach the app can take it. Claim it before you expose the instance.