← Blog

Three small things that make a cafe bot feel finished

September 6, 20264 min read

The last two releases of michi-chat added no new capability to the assistant. It cannot do anything today that it could not do last week. And yet the owner who runs it for a cafe in Pasig noticed every change within an hour, because each one removed a small daily friction. This post is about three of those, and about the rule that connects them: the finished feeling of a product lives in the seams, not in the features.

1. Contact details you can tap

The assistant has always been good at pointing people to the right channel: "the best way to book is the inquiry form; email works too; the phone exists but is not always answered." Correct, honest, and useless on a phone, because the email address and the number were plain text. The visitor had to select, copy, switch apps, paste.

The visitor chat answering a booking question, with the email and phone number underlined as links

Now a bare web address, an email or a Philippine phone number in an answer becomes a real link: tap to open, tap to mail, tap to call. Every link opens in a new tab, because the chat usually lives inside a small frame on the business website and navigating that frame away would eat the conversation.

The implementation is the interesting bit for the builders reading this. Answers are rendered from markdown, and the markdown renderer had no idea a phone number was a link. Rather than pull in a plugin, a forty-line pass walks the parsed text and splits it into text and link segments, skipping anything that is already a link or code. Two guards mattered more than the matching itself:

  • Digit counting. A cafe answer is full of numbers: 3,500 pesos, 3 hours, 50 cups, 2026. The phone pattern only fires on a plus sign or a leading zero followed by nine to thirteen digits with spaces or dashes, so prices, years and cup counts stay text. The unit tests are a list of things that must NOT become a phone link.
  • Protocol allowlist. The renderer's default safe list of link schemes did not include tel:, and adding it meant writing the transform myself. The transform allows exactly https, http, mailto and tel, and blanks anything else with a scheme, so a javascript: link smuggled in through a knowledge document can never render.

2. A glossary under the bars

The per-tenant analytics page has a bar chart called "What visitors reach for": how often the assistant used each of its lookups over the last month. The bars were labelled with function ids, search_kb, get_menu, get_weather, because that is what the model calls them. The owner read it and asked, reasonably, what each one takes and gives back.

The analytics card with a plain-words glossary under each bar: label, description, input, returns

The fix was not to write documentation. It was to make documentation impossible to skip. Every tool pack in the codebase now has a required returns sentence, so a pack without one does not compile, and the glossary reads the label, the description, the inputs and that sentence from the same definition the model receives. The page cannot drift from the code because it is the code, rendered for a person.

3. Staff who see only their tenant

michi-chat runs several businesses on one instance. Until this week a staff login saw all of them: every transcript, every knowledge base. That was fine while every staff member was also the operator. It stops being fine the moment a cafe hires someone.

The accounts page, with a checkbox per tenant on a staff row and a Save button under them

An owner now ticks the tenants a staff account may see, on the create form or inline in the accounts table. The design decisions, in the order they were made:

  • No tenants means nothing. A brand-new staff account sees an empty admin until an owner assigns it. The safe default is the one that shows less.
  • Read live, not snapshotted. The role is stamped into the session at login; the tenant list is read on every request instead, so when an owner narrows an account it takes effect on that account's next click, not after a twelve-hour session expires.
  • Enforced in the queries, not the menu. Hiding links is courtesy. The actual boundary is one helper that every staff-reachable query ANDs into its WHERE clause, plus a check on every page and action that is addressed by tenant id. A tenant outside your scope does not exist: it is a 404, not a "you may not".
  • Upgrade warning in the release notes. Existing staff accounts have no tenants after the upgrade. The changelog says so in bold, because the alternative is an owner discovering it from a confused colleague.

The rule underneath

None of these is a feature you would put on a landing page. Each one came from watching a real person use the thing: an owner reading a bot reply on a phone, an owner reading a chart, an owner about to give a login to someone else. The work was small, a day in total, and the bar it had to clear was not "does it work" but "would you notice if it were missing". That is a different bar, and it is the one that decides whether software feels finished.

If you run a small business and want an assistant like this on your site, the owner's guide starts from zero; it is being moved to a new home and will be linked here again. If you build things, the changelog has the commit-level version of this story.