← Blog

A second kind of tenant

September 6, 20263 min read

For its first month michi-chat had exactly one idea of what a tenant is: a customer assistant for one business. It answers questions about the cafe, the menu, the hours, and it declines everything else in one sentence. That refusal is not a bug; it is most of what makes the thing safe to put on a website.

Then a different project of mine needed an AI coach. Chess Mate analyses your games with a chess engine and wanted a coach that explains, in plain words, why the engine disliked a move. The obvious thing was to run that coach on michi-chat too: same conversations, same memory, same usage accounting, same admin. The obvious thing did not work, because the platform preamble told every tenant to refuse anything that was not about "the business".

Three ways to solve it, two of them wrong

  • A plugin system. The README lists this as a non-goal, and for a reason: a plugin is code that runs inside the container, next to the database, on behalf of a tenant. The security model rests on tenants supplying parameters, never code.
  • A fork. Chess Mate could run its own copy with a different preamble. But the whole deployment story is one public image that anyone can run, and a fork breaks that on day one.
  • A small, additive change to the core. A tenant gets a kind. business is the default and behaves exactly as before. coach is the new one.

The Kind field on the tenant form, set to coach, with the help text explaining the difference

What a coach is allowed to do

A coach is driven by an application, not by strangers on a website. The application sends facts with every message; in Chess Mate's case the engine's verdict on the position and the moves it preferred. The coach's job is to turn those facts into an explanation a learner can use, and to answer follow-up questions freely within its subject.

So the coach preamble drops two things and keeps everything else:

  • Dropped: the "you are the assistant for one business" framing, and the off-topic refusal. A coach may talk about chess as long as the learner likes.
  • Kept: every protection. It never leaks its instructions, it treats tool data as data rather than as commands, it refuses abuse, it will not discuss its own architecture, and it does not use dashes. Those are platform rules; a kind cannot loosen them.

Two mechanical differences follow from the job. Messages may be up to 6000 characters, because the facts an application sends take room. And the semantic answer cache is off for coaches: two engine reports about different positions read as near-paraphrases to an embedding model, and a cached explanation of the wrong position is worse than no cache. The refusal and fallback lines are also worded per kind, so a coach that cannot help says so like a coach, not like a shop.

Keeping it invisible for everyone else

The owner's concern was fair: most people running michi-chat run a business assistant and should not meet a Kind dropdown they do not need. So the field only appears when the instance opts in with an environment variable listing the kinds it enables. A plain install looks exactly as before. And a hidden field never resets a stored kind, which is the sort of detail that only matters the first time an owner edits a coach tenant on an instance that has not opted in. Two unit tests pin that down.

The tenant JSON that moves a tenant between instances carries its kind too, and there is now a command-line import that prints the public key, because the coach tenant is created by a deploy script rather than by a person clicking through the admin.

Why this is the right size

The change is one column, one migration, one branch in the prompt builder and a handful of kind-worded strings. Nothing about the business assistant changed; its preamble is byte-identical, which the tests check. That is the test I apply to any request that starts with "could the platform also": if the answer needs a new mechanism, it is probably a different product; if it needs a new value for a column that already existed in spirit, it belongs here.

The coach has been explaining lost games for a few days now. It remembers the conversation across moves, it is honest when the engine's reasoning is beyond it, and it lives in the same admin as a cafe, which still makes me smile.