TECHNICAL

When Your Slack Workspace Becomes the War Room

February 24, 202615 MIN READ

The Issue

Here is a universal truth of business operations: your team has 10 dashboards open, three CRM tabs, a payment portal, an analytics tool, and exactly one app where they actually spend their day -- Slack.

Slack is a paradox. It is simultaneously where the most productive conversations happen and where productivity goes to die in a sea of emoji reactions. But fighting it is futile. The sales and ops teams lived in Slack the way fish live in water. The problem was that business-critical events (payments, call summaries, session reports, lead assignments) were scattered across Razorpay dashboards, internal admin panels, and spreadsheets that somehow always had yesterday's data. By the time someone noticed a failed payment or a missed session, the moment had passed.

The Goal

Turn Slack from a chat app into a real-time operations nerve center. Payment events should appear as threaded messages with lifecycle updates. Call summaries should route to the right team lead's private channel. Session reports should post to course-specific channels every week. Workspace membership should auto-sync with student enrollment. All without anyone opening another tab.

The Solution

1. OAuth Integration. Full Slack OAuth 2.0 flow. Admin users authenticate, get redirected to Slack's consent screen with scopes like chat:write, channels:write, groups:write, and users:read, then return via callback. We exchange the code for a user token, match it to an admin user by email, and persist the token. Messages are posted as them -- not from a faceless bot, but from their actual Slack identity.

2. Payment Lifecycle Threads. The crown jewel. When a learner initiates a payment, we post a "Payment Initiated" message to Slack with lead name, amount, and currency. We store the Slack message timestamp (ts) in an AdminSlackMessageLog table. When Razorpay fires a webhook for payment.captured or payment.failed, we validate the signature, check Redis for the event ID to prevent duplicate processing, look up the stored ts, and reply in the same thread with the outcome. One thread, full lifecycle.

For part payments vs full payments, we route to separate channels. The routing logic checks LeadPartPayment records -- if a learner has completed one installment, subsequent events go to SLACK_PART_PAYMENT_CHANNEL_ID.

3. Team-Based Routing via Org Hierarchy. Call summaries reach the agent who made the call plus their entire reporting chain. We use an AdminUserClosure table (closure table pattern) to resolve every ancestor. Private Slack channels are created per agent, membership is synced on every summary post -- adding new managers, removing departed ones.

4. Weekly Session Reports. A cron runs every Monday and Friday at 9 AM IST. It queries sessions from the previous window, aggregates attendance counts, average ratings, and written feedback, then posts formatted Slack Block Kit messages to course-specific channels via CourseAnnouncementChannelMap. Low-rated feedback gets highlighted separately.

5. Workspace Invite Automation. A UserSlackDetails model tracks invite lifecycle: PENDING -> INVITED -> ACCEPTED -> DEACTIVATED. A sync function paginates through Slack's users.list API, classifies each member by their status flags, and reconciles against the database. Enrolled learners without an invite get one automatically.

Architecture

Loading diagram...

Complexities Faced

Thread state management was the subtlest challenge. Slack's threading relies on the original message's ts as a thread identifier. Losing that ts between the "initiated" and "captured" events means orphaned messages. We initially used Redis with a TTL, then migrated to AdminSlackMessageLog in Postgres for durability -- because a payment webhook can arrive minutes or hours later, well past any reasonable cache TTL.

Webhook idempotency was non-negotiable. Razorpay's webhook delivery is at-least-once, and processing a payment event twice means duplicate Slack messages and confused sales reps. Redis-backed event ID deduplication with a 10-minute TTL solved this cleanly.

Channel routing conditionals grew complex fast. A single payment event's destination depends on: course type, payment type (part vs. full), prior payment history, and admin assignment. Less a technical problem than a "draw the decision tree on a whiteboard before writing code" problem.

Org hierarchy sync required defensive coding. Channel members change as the org restructures. The sync diffs expected members (from the closure table) against actual members (from Slack's API), handles cant_kick_self errors gracefully, and reports failures to Sentry rather than crashing.

What I Learned

Building on Slack's API is deceptively easy for the first message and deceptively hard for the hundredth use case. Thread-based updates are powerful but demand careful state management. The ts field is your thread's primary key -- treat it with the same respect you'd give a database ID.

But the biggest win was not technical. It was organizational. Bringing payment events, session reports, and call summaries into Slack eliminated an entire category of "I didn't see it in the dashboard" problems. When the data lives where the team lives, response times collapse from hours to minutes. Slack went from the place where work gets discussed to the place where work gets done.