< the skull

BUILDING THE BRIDGE: FROM DIFY TO CHATWOOT
==========================================
2026-06-29

I spent a weekend wiring an AI bot into a customer support inbox, and the part
that ate most of my time was not the AI and not the inbox. It was the small
piece of software that sits between them. Nobody talks about that piece much,
which is strange, because it is the piece that makes the whole thing work. Here
is how it went.

The setup is common enough. On one side you have a help desk that collects
conversations from a website widget and a messaging channel. On the other you
have an AI platform that holds your knowledge base and answers questions. Both
are mature products. Both have APIs. You would assume someone has already glued
them together in a clean, maintained package.

They have not, or at least not in a way I would trust in production. What I
found instead were a couple of community projects that looked promising for
about ten minutes. One was still in beta and had not been touched in a long
while. Its companion repository was gone entirely. Another dragged in a heavy
stack of background workers and an extra database just to pass a message from
one place to another. For a job this simple, that felt wrong.

What the middleware actually does
=================================

Strip away the jargon and the job is almost boring. A customer sends a message.
The help desk pokes your service to say a message arrived. Your service reads
it, asks the AI for an answer, and posts that answer back into the conversation.
That is the entire loop.

The first instinct is to write the part that calls the AI. Resist it. The first
thing your service should be good at is saying no. A help desk fires events for
all kinds of reasons, and most of them are not your concern. The bot's own
replies generate events too, and if you are not careful your service will answer
itself in a loop that is both funny and expensive.

I settled on a tight filter near the top of the request handler. Act only on
genuinely new messages from the customer, and only while the conversation is
still in the bot's hands. Everything else gets a polite acknowledgment and
nothing more. Getting this filter right early saved me from a whole category of
bugs that would have been miserable to chase later.

There is a timing trap here that is worth naming. The help desk expects a quick
reply when it pokes your service. The AI, meanwhile, can take several seconds to
think, especially when the answer runs long. If you make the help desk wait for
the AI, you risk the request timing out, and timeouts in this kind of plumbing
are the worst sort of intermittent problem.

The fix is to acknowledge the poke immediately and do the real work in the
background. Tell the help desk you received the message, close that request, and
only then go bother the AI. The customer waits a few seconds for a thoughtful
answer, which is fine. Your service never leaves anyone hanging on an open
connection.

An AI answer is much better when it knows what was already said. Most platforms
give you a handle, some kind of identifier, that ties a series of messages into
one ongoing conversation. Your job is to remember which platform conversation
belongs to which help desk conversation, and to hand that identifier back each
time.

You do not need much to store this. A tiny local database file, the kind that
lives on disk and needs no server of its own, is plenty. People reach for a full
database engine by reflex, but for a lookup table this small that is overkill.
Keep the storage as light as the job demands.

A bot that cannot get out of the way is worse than no bot. Build the exit early.
There should be a few clear ways a conversation leaves the bot and reaches a
person. The customer asks for a human in plain language. The AI signals that it
is out of its depth. Or the AI simply fails, in which case a human should catch
the conversation rather than the customer hitting a wall.

Mechanically the handoff is just flipping the conversation's state so the bot
stops paying attention and the team can pick it up. The hard part is not the
mechanics, it is remembering to treat failure as a normal path rather than an
afterthought. When the AI errors out, the customer should glide to a person, not
see a broken silence.

Even if you start with a single bot on a single channel, leave room for more.
The cheap way to do this is a small routing table that maps each inbox to the AI
app that should serve it. One channel today, three channels and three different
bots tomorrow, and the routing table absorbs the change without you rewriting
the core. This costs almost nothing to set up on day one and saves a real
rewrite later.

The part I want to be honest about is that the clean architecture was not what
cost me the night. The surprises were. A platform update had quietly added a
security rule that refused to deliver messages to internal network addresses,
which is exactly where a service like this tends to live. The behavior looked
like the bot was broken when in fact the message was being blocked one step
upstream, by a protection that was doing its job a little too eagerly.

None of these were architecture problems. They were the kind of thing you only
find by reading logs slowly and refusing to guess. When the bot goes quiet, the
logs are already telling you why. Trace the message one hop at a time and let
the system show you where it stopped.

The bridge is small. It is also the difference between two impressive tools that
ignore each other and one quiet system that answers your customers at three in
the morning. Worth building carefully.


< the skull