Kyndium

Why Slack posts come from the database

Kyndium's Slack notifications are sent by Postgres triggers, not by the app. Here is why that is the only way to cover every path, and what it took to make it safe.

The Kyndium team2 min readengineeringslackpostgres

A Slack notification for "a task was completed" sounds like a few lines of code in the function that completes tasks. The trouble is that Kyndium has many such functions. A task completes from the Checklist's check, from a done lane on the Board, from the Timeline at 100%, from Home, from "Complete them" on a lane, from a sprint, from an import, from an automation. An app-level notification would have to be wired into every one of them and remembered for every new one.

So the notification is not in the app. It is in the database.

Triggers that post

Postgres can make HTTP requests through pg_net, which queues them in a table and sends them asynchronously. Kyndium attaches triggers to the tasks and comments tables: a completion trigger on updates, a statement-level insert trigger with a transition table, and a comment insert trigger. Each one calls a single function that formats the message, escapes it for Slack, adds a link and the project's name, and posts it to every channel connected to that project that wants that kind of event.

Because the trigger fires on the row change itself, every path is covered by construction. A path that does not exist yet is covered too.

Keeping it safe

Three things keep this from being a liability.

The post never blocks the change. The function catches everything and never raises; a Slack outage cannot stop a task from completing.

Bursts fold. The insert trigger sees the whole statement, so an import of 400 tasks posts "added 400 tasks" rather than 400 messages, and each channel is capped at thirty posts a minute.

The webhook stays secret. The URL lives in an owner-only table, is validated against Slack's hostname, is never returned whole to the browser after it is saved, and the function that posts cannot be called by users at all.

A side effect worth having

Since the queue is a table, a rolled-back transaction sends nothing, which made the whole thing testable: run the change inside a transaction, inspect the queue, roll back. And the same mechanism gives the morning digest, run by a daily job that calls one function, for free.

Was this useful?

Comments