Bot Meets CMS: Building a Thread-Based Publishing Bridge

Connecting the Dots: How I Unified a Bot and Strapi Into One Publishing System
The bot-social-publisher had been humming along, publishing development notes, but something was missing. Notes were landing in Strapi as isolated entries when they should have been grouped—organized into threads where every note about the same project lived together with shared metadata, tags, and a running digest. The problem: the bot and the CMS were speaking different languages. Time to make them fluent.
I started with a safety check. Seventy tests in the suite, all passing, one skipped. That green bar is your permission slip to break things intelligently.
The backend half was already sketched out in Strapi—new endpoints accepting thread_external_id to link notes to containers, a PUT /api/v1/threads/:id route for updating thread descriptions. But the bot side was the real puzzle. Every time the bot published a second note for the same project, it had no memory of the thread it created for the first note. So I added a thread_sync table to SQLite—a simple mapping layer that remembers: “project X belongs to thread with external ID Y.”
That’s where the ThreadSync module came in. The core idea was almost mundane in its elegance: cache thread IDs locally to avoid hitting the API repeatedly. Methods like get_thread_for_project() checked the database first. If nothing existed, ensure_thread() would create the thread remotely via the API, then stash the mapping for next time. Think of it as a telephone book for your projects.
The tricky part was weaving this into the publication flow without breaking the pipeline. I needed to call ensure_thread() before constructing the payload, grab the thread ID, pack it into the request, then—here’s the clever bit—after the note published successfully, trigger update_thread_digest(). This function pulled metadata from the database, counted features and bug fixes, formatted a bilingual summary (“3 фичи, 2 баг-фикса” alongside “3 features, 2 bug fixes”), and pushed the update back to Strapi.
All of this lived inside WebsitePublisher, initialized with the ThreadSync instance. Since everything needed to be non-blocking, I used aiosqlite for async database access. No waiting, no frozen threads.
Here’s what struck me: Strapi is a headless CMS, typically just a content container. But I was asking it to play a structural role—threads aren’t folders, they’re first-class API entities with their own update logic. That required respecting Strapi’s patterns: knowing when to POST (create) versus PUT (update), leveraging external_id for linking external systems, and handling localization where Russian and English descriptions coexist in a single request.
The commit was straightforward—three files changed, the rest was CRLF normalization noise from Windows fighting Unix. Backend deployed. The system breathed together for the first time: bot publishes, thread syncs, digest updates, all visible at borisovai.tech/ru/threads.
The lesson sank in as I watched the test suite stay green: good architecture doesn’t mean building in isolation. It means understanding how separate pieces speak to each other, caching intelligently, and letting synchronization happen naturally through the workflow rather than fighting it.
Seventy tests passing. One thread system connected. Ready for the next feature. 😄
Metadata
- Session ID:
- grouped_C--projects-bot-social-publisher_20260210_1829
- Branch:
- main
- Dev Joke
- Почему Traefik расстался с разработчиком? Слишком много зависимостей в отношениях