Building Real-Time Sync Between Slack and Your Task Manager
A technical deep-dive into how we built bidirectional sync between Slack and our ticket system using webhooks, message queues, and event-driven architecture.
Introduction
One of the most requested features from our users was Slack integration. Not just notifications, but real bidirectional sync. When a ticket updates, Slack knows. When someone replies in Slack, the ticket updates.
Building this turned out to be more interesting than we expected. In this post, we will walk through our architecture, share some code, and explain the tradeoffs we made.
The Architecture Overview
Our sync system has three main components: a webhook receiver for Slack events, a message queue for reliable processing, and a sync engine that handles the bidirectional updates.
Here is the high-level flow:
Slack Event -> Webhook Receiver -> Message Queue -> Sync Engine -> Database
|
v
Ticket Updated
|
v
Slack API <- NotificationThis architecture ensures we never lose events, even during high load or temporary outages.
Handling Slack Webhooks
Slack sends events via HTTP POST to your webhook endpoint. The tricky part is responding quickly - Slack expects a 200 response within 3 seconds, or it will retry.
Our solution: acknowledge immediately, process asynchronously.
export async function handleSlackWebhook(req: Request) {
const payload = await req.json();
if (!verifySlackSignature(req, payload)) {
return new Response("Invalid signature", { status: 401 });
}
if (payload.type === "url_verification") {
return Response.json({ challenge: payload.challenge });
}
await messageQueue.publish("slack-events", {
event: payload.event,
teamId: payload.team_id,
timestamp: Date.now(),
});
return new Response("OK", { status: 200 });
}The key insight: separate receiving from processing. Your webhook handler should do almost nothing except queue the work.
The Sync Engine
The sync engine is where the magic happens. It consumes events from the queue and determines what action to take.
async function processSyncEvent(event: SlackEvent) {
const { type, channel, message, thread_ts } = event;
const ticket = await findLinkedTicket(channel, thread_ts);
if (!ticket) return;
switch (type) {
case "message":
await addCommentToTicket(ticket.id, {
content: message.text,
author: await resolveSlackUser(message.user),
source: "slack",
});
break;
case "reaction_added":
if (message.reaction === "white_check_mark") {
await updateTicketStatus(ticket.id, "done");
}
break;
}
}We use a simple convention: a checkmark emoji marks a ticket as done. This lets team members update ticket status without leaving Slack.
Preventing Infinite Loops
The biggest gotcha with bidirectional sync: infinite loops. Ticket updates Slack, Slack event triggers ticket update, which updates Slack, forever.
Our solution uses event sourcing with origin tracking:
await updateTicket(ticketId, {
status: "done",
_meta: {
source: "slack",
sourceEventId: event.event_ts,
}
});
async function notifySlack(ticket: Ticket, change: Change) {
if (change._meta?.source === "slack") {
return;
}
await slack.chat.postMessage({
channel: ticket.slackChannel,
text: formatTicketUpdate(change),
thread_ts: ticket.slackThreadTs,
});
}Every mutation carries metadata about its origin. Before propagating a change, we check if it came from the destination.
Handling Rate Limits
Slack has strict rate limits. We use exponential backoff and a token bucket algorithm:
const rateLimiter = new TokenBucket({
capacity: 50,
refillRate: 1,
refillInterval: 1000,
});
async function postToSlack(message: SlackMessage) {
await rateLimiter.acquire();
return slack.chat.postMessage(message);
}Other edge cases we handle: message ordering (using timestamps), deleted messages (soft-delete with indicator), and network failures (retry with backoff).
Monitoring and Debugging
Distributed systems are hard to debug. We built observability in from the start:
logger.info("sync.completed", {
ticketId: ticket.id,
slackChannel: channel,
eventType: event.type,
processingTimeMs: Date.now() - startTime,
queueLatencyMs: startTime - event.timestamp,
});We track queue depth, processing latency, and error rates. When sync breaks, we know within seconds.
Conclusion
Building real-time sync is challenging but rewarding. The key principles: separate ingestion from processing, track event origins to prevent loops, and build observability from day one.
Our Slack integration has become one of our most-used features. See the full Slack integration guide to get started in minutes.
Want to see what else you can automate? Read our post on how AI handles tickets automatically.

Jordan
Co-founder
Related posts
The Future of Freelancing: Let AI Handle Your Tickets
How AI-powered ticket automation is transforming the way freelancers and small teams manage client work, from request to resolution.
5 Hidden Features in Refront That Save Hours Every Week
Discover the lesser-known features in Refront that can dramatically reduce your admin time and boost productivity.

Why we built Refront
From frustrated entrepreneurs to 200% more revenue. This is our story and why we believe work can be different.