Build a Chat to Slack Integration for Real-Time Feedback

Timothy Olanrewaju
Timothy Olanrewaju
Published August 18, 2025

To build great products, start by listening. Everything matters. Every bug report, feature request, or glowing review. These are the signals that fuel your product roadmap.

However, standing between success and failure is the pain of managing and routing that feedback to the right teams.

According to a Zendesk report, 59% of support teams are overwhelmed. Feedback pours in from multiple channels, often causing delays and leaving users feeling ignored. Support teams want to help; they try to, but the fragmented channels jeopardize their efforts. When this happens, nobody wins.

What if feedback didn’t float around in limbo or reside in silos but instead went straight to the people who can fix the problem—right inside Slack. Slack is a well-known and widely recognized workspace tool for developers, project managers, and support teams. And when feedback is sent to a dedicated Slack channel, it’s more likely to be seen, fast.

In this tutorial, we will implement a feedback system that uses Stream Chat to gather user thoughts and sends them directly into Slack.

Here’s a quick look at what we’ll build:

All of the code for this tutorial can be found in this repo.

Technical Prerequisites

Before we begin, ensure you have:

Create Your Slack App

We need to create a Slack app to send messages from Stream Chat to Slack. This app lets your code securely talk to Slack’s API and send feedback to a specific channel. Consider it the connection point between your Stream Chat messages and your team’s Slack workspace.

A Slack app enables you to:

  • Secure an API token.
  • Have your application post messages.
  • Control which channels or data your app can access.

Let’s walk through the steps to set up your Slack app:

  1. Go to Slack’s API portal and click the “Create New App” button.
  2. From the resulting pop-up, click on the “From Scratch” option.
  3. Choose an app name and the workspace in which you’d like to develop the app.
  4. Click on “Create App”.
  5. Once your app is created, go to the OAuth & Permissions tab.
  6. Under Scopes, add the following Bot Token Scopes:
    • chat:write - to allow the app to send messages to channels
    • channels:read - if you want to post to public channels
  7. In the OAuth Tokens section, click on “Install to {{workspace_name}}”.
  8. On the next auth page, click on “Allow”.
  9. You’ll get a Bot User OAuth Token. Copy the token and save it somewhere safe.
  10. Enter the workspace you integrated with your Slack app and create a channel.
  11. Click on the channel. Copy the last part on your browser’s URL, usually prefixed with C (e.g, C092WJFRJGZ). That is your channel ID.

Project Setup

We create and set up our React project with the Stream React Chat SDK. We'll use Vite with the TypeScript template:

bash
1
2
3
npm create vite user-feedback-collector -- --template react-ts cd user-feedback-collector npm i stream-chat stream-chat-react axios

Next, we set up a Node.js backend by creating a server directory in the project root:

bash
1
2
3
4
mkdir server cd server npm init -y npm install express @slack/web-api dotenv cors

Finally, we set up our environment variables:

  • In the server directory, create a .env file and provide the necessary credentials:
bash
1
2
3
4
5
SLACK_BOT_TOKEN=your_slack-bot_token SLACK_CHANNEL_ID=your_channel_id STREAM_API_KEY=your_stream_api_key STREAM_API_SECRET=your_stream_api_secret PORT=5000
  • In the project’s root, create a .env file and provide your Stream API key:
bash
1
2
VITE_STREAM_API_KEY=your_stream_api_key VITE_API_URL=http://localhost:5000

Backend Architecture

The Node.js and Express stack handles server-side logic and handles HTTP requests within this application for real-time feedback processing.

Server Initialization and Configuration

At startup, the server checks for critical environmental variables. These variables are:

  • SLACK_BOT_TOKEN
  • STREAM_API_KEY
  • STREAM_API_SECRET
  • SLACK_CHANNEL_ID.

The check is important as it ensures a secure and reliable operation. If any are missing, the server terminates to prevent incorrect configuration.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
// server/index.js const requiredEnvVars = [ 'SLACK_BOT_TOKEN', 'STREAM_API_KEY', 'STREAM_API_SECRET', 'SLACK_CHANNEL_ID' ]; const missingEnvVars = requiredEnvVars.filter(varName => !process.env[varName]); if (missingEnvVars.length > 0) { console.error('Missing required environment variables:', missingEnvVars.join(', ')); process.exit(1); }

With this, the backend establishes critical integrations with Stream Chat and Slack.

Slack and Stream Client Initialization

A Slack Web API Client authenticates with a bot token to enable real-time feedback to a designated Slack Channel. We must also create a Stream Chat client using Stream’s API credentials to manage users, channels, and messages.

js
1
2
3
4
5
6
// server/index.js const { WebClient } = require('@slack/web-api'); const { StreamChat } = require('stream-chat'); const slack = new WebClient(process.env.SLACK_BOT_TOKEN); const streamClient = StreamChat.getInstance(process.env.STREAM_API_KEY, process.env.STREAM_API_SECRET);

Stream Chat Integration

The integral aspect of this system relies on Stream Chat. Each customer is given a unique ID (e.g., customer-\<id>) and a dedicated messaging channel (e.g., feedback-\<customerId>). We’ll also have a support-bot user, initialized as an admin, and participating in every channel to send automated responses.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// server/index.js async function createStreamUser(userId, name, role = 'user') { try { await streamClient.upsertUser({ id: userId, name: name || `Customer ${userId}`, role: role, image: role === 'admin' ? 'https://via.placeholder.com/40x40.png?text=BOT' : 'https://via.placeholder.com/40x40.png?text=USER' }); return true; } catch (error) { return false; } } // Initialize support bot user async function initializeSupportBot() { try { await streamClient.upsertUser({ id: 'support-bot', name: 'Support Team', role: 'admin', image: 'https://via.placeholder.com/40x40.png?text=BOT' }); return true; } catch (error) { return false; } } // Get or create customer channel async function getOrCreateCustomerChannel(customerId) { try { const channelId = `feedback-${customerId}`; const channel = streamClient.channel('messaging', channelId, { name: `Feedback from ${customerId}`, custom: { type: 'feedback', customerId: customerId }, members: [customerId, 'support-bot'] }); await channel.create(); return channel; } catch (error) { return null; } }

Feedback Submission

The feedback submission process is the central workflow, orchestrated by the /api/feedback/submit endpoint. When a customer submits feedback, the backend:

  • Validates the input (customerId and message).
  • Creates a Stream Chat user for the customer if they don’t exist.
  • Establishes or retrieves a dedicated feedback channel for the customer.
  • Send the customer’s message to the channel.
  • Follows up with an automated support-bot response.
  • Posts a notification to Slack with the feedback details.
Get started! Activate your free Stream account today and start prototyping your chat app.

This flow ensures real-time communication and enables visibility for both parties.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
app.post('/api/feedback/submit', async (req, res) => { const { customerId, message } = req.body; try { if (!customerId || !message) { return res.status(400).json({ error: 'Missing required fields', required: ['customerId', 'message'] }); } const messageText = typeof message === 'string' ? message : String(message); const formattedCustomerId = customerId.startsWith('customer-') ? customerId : `customer-${customerId}`; // Create customer user const customerCreated = await createStreamUser( formattedCustomerId, `Customer ${customerId}`, 'user' ); if (!customerCreated) { return res.status(500).json({ error: 'Failed to create customer user' }); } // Get or create channel const channel = await getOrCreateCustomerChannel(formattedCustomerId); if (!channel) { return res.status(500).json({ error: 'Failed to create channel' }); } // Send customer message await channel.sendMessage({ text: messageText, user_id: formattedCustomerId, custom: { type: 'customer_message', timestamp: new Date().toISOString() } }); // Send bot response await channel.sendMessage({ text: 'Thank you for your feedback! Our team will review and respond soon.', user_id: 'support-bot', custom: { type: 'bot_response', timestamp: new Date().toISOString() } }); // Send to Slack try { await slack.chat.postMessage({ channel: process.env.SLACK_CHANNEL_ID, text: `💬 *New Chat Message*\n*From:* ${formattedCustomerId}\n*Message:* ${messageText}` }); } catch (slackError) { // Don't fail the request if Slack fails } res.status(200).json({ success: true, message: 'Message sent successfully', customerId: formattedCustomerId, channelId: `feedback-${formattedCustomerId}` }); } catch (error) { res.status(500).json({ error: 'Failed to submit message', details: error.message }); } });

The backend provides additional endpoints to support the application:

  • /api/health: Checks connectivity with Stream Chat and Slack, ensuring system health.
  • /api/stream-token: Generates authentication tokens for Stream Chat clients.

Run the Server

The application runs on port 5000 by default. Run the server by entering this command:

bash
1
node index.js

Frontend Architecture

The frontend of this application aims to deliver a responsive, real-time chat interface for users to submit feedback using Stream Chat’s React SDK. It works hand-in-hand with the backend to authenticate users, manage sessions, and display a toggleable chat widget located at the bottom right of the web page.

The App Component

Like all React applications, the App component serves as the root. It is where we initialize the Stream Chat React SDK using the StreamChat.getInstance method.

js
1
const client = StreamChat.getInstance(import.meta.env.VITE_STREAM_API_KEY);

State Initialization and User Session

The App component manages state using the useState and useEffect hooks to handle connection status, loading states, and widget visibility. A unique currentUserId is generated for each chat session with a combination of timestamp and random string, ensuring distinct user identification.

We use the connectionError, isLoading, and isWidgetOpen states to track the chat connection, initialization, and widget visibility.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/App.tsx import { useState, useEffect, useCallback } from 'react'; import { useCreateChatClient, Chat } from 'stream-chat-react'; import './App.css'; import ChatWidget from './components/ChatWidget'; function App() { const [connectionError, setConnectionError] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(true); const [isWidgetOpen, setIsWidgetOpen] = useState(false); const [currentUserId] = useState(() => { const timestamp = Date.now().toString(36); const randomStr = Math.random().toString(36).substr(2, 5); return `customer-${timestamp}-${randomStr}`; });

Connecting to Stream Chat

The connection to Stream Chat is established using the useCreateChatClient hook with a token provider function. The connection process performs the following:

  1. Defines the tokenProvider function that checks the backend health via /api/health/ endpoint.
  2. Requests a Stream Chat token from the backend’s /api/stream-token endpoint using the generated currentUserId.
  3. Initializes the Stream Chat client using the useCreateChatClient hook with API key, token, provider, and user data.
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// src/App.tsx const tokenProvider = useCallback(async () => { try { const apiUrl = getApiUrl(); const healthResponse = await fetch(`${apiUrl}/api/health`); if (!healthResponse.ok) { throw new Error(`Backend health check failed: ${healthResponse.status}`); } const response = await fetch(`${apiUrl}/api/stream-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ userId: currentUserId }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to get Stream token: ${response.status} - ${errorText}`); } const data = await response.json(); if (!data.token) { throw new Error('No token received from backend'); } return data.token; } catch (error) { console.error('Token provider error:', error); throw error; } }, [currentUserId]); const user = { id: currentUserId, name: `Customer ${currentUserId.split('-')[1]}`, }; const client = useCreateChatClient({ apiKey: import.meta.env.VITE_STREAM_API_KEY, tokenOrProvider: tokenProvider, userData: user, }); useEffect(() => { if (client) { setIsLoading(false); setConnectionError(null); const handleConnectionError = (error: any) => { console.error('Stream connection error:', error); setConnectionError(`Connection failed: ${error.message}`); }; client.on('connection.error', handleConnectionError); return () => { client.off('connection.error', handleConnectionError); }; } }, [client]);

Rendering the User Interface

Our UI would include a toggle button that controls the visibility of the ChatWidget component, which renders the Stream Chat interface. During initialization, a loading screen displays the currentUserId and API URL. Once the user is connected to Stream Chat, the ChatWidget is conditionally rendered when isWidgetOpen is true, ensuring a clean and non-intrusive user experience.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if (isLoading) { return ( <div className="loading-container"> <h3>Connecting to chat service...</h3> <p>User ID: {currentUserId}</p> <p>API: {getApiUrl()}</p> </div> ); } return ( <Chat client={client} theme="messaging light"> <div className="app-container"> <header className="app-header"> <h1>Welcome to User Feedback Collector</h1> <h3 className="session-id">Your session ID: {currentUserId}</h3> <h3 className="connection-status">✅ Connected to chat service</h3> </header> <button className="chat-toggle-button" onClick={toggleWidget} data-state={isWidgetOpen ? 'open' : 'closed'} aria-label={isWidgetOpen ? 'Close chat widget' : 'Open chat widget'} ></button> {isWidgetOpen && ( <div className="chat-widget-container"> <ChatWidget customerId={currentUserId} /> </div> )} </div> </Chat> );

The ChatWidget Component

The ChatWidget is at the centre of our front-end real-time chat interface. It serves as the user-facing component working with Stream Chat and Slack. It renders a fully interactive chat window where customers can submit feedback and receive automated responses.

Component Setup and Context

This ChatWidget component receives a customerId prop, identifying the user interacting with the chat. It leverages Stream Chat's useChatContext hook and automated channel management to handle the chat interface and message routing. The component performs the following operations:

  1. Retrieves the Stream Chat client from the chat context using useChatContext.
  2. Creates a channel instance using useMemo with a formatted customer ID.
  3. Defines channel members as the customer and a support-bot for automated responses.
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/components/ChatWidget.tsx const ChatWidget: React.FC<ChatWidgetProps> = ({ customerId }) => { const { client } = useChatContext(); const channel = useMemo(() => { if (!client || !customerId) return null; const formattedCustomerId = customerId.startsWith('customer-') ? customerId : `customer-${customerId}`; const channelId = `feedback-${formattedCustomerId}`; return client.channel('messaging', channelId, { members: [formattedCustomerId, 'support-bot'], }); }, [client, customerId]);

Message Submission and Backend Integration

The ChatWidget overrides the default message submission behavior of Stream Chat’s MessageInput component with a custom handleMessageSubmit function.

This function:

  • Ensures the message the user enters is a non-empty string, preventing invalid submissions.
  • Constructs a payload with the formattedCustomerId and message text.
  • Logs API or network errors without disrupting the UI.

This integration is important as it ensures messages are processed through the backend, enabling features like Slack notifications and automated bot responses.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const handleMessageSubmit = async (params: { cid: string; localMessage: any; message: any; sendOptions: any; }) => { const messageText = params.message.text; if (!messageText || typeof messageText !== 'string' || !messageText.trim()) { return; } const formattedCustomerId = customerId.startsWith('customer-') ? customerId : `customer-${customerId}`; const payload = { customerId: formattedCustomerId, message: messageText.trim(), }; try { const response = await fetch(`${import.meta.env.VITE_API_URL}/api/feedback/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (!response.ok) { console.error('API Error:', response.status); } } catch (error) { console.error('Network error:', error); } };

Rendering User Interface and Stream Chat Components

The ChatWidget renders a fully functional chat interface using Stream Chat’s pre-built components: Channel, Window, ChannelHeader, MessageList​​, and MessageInput.

The MessageInput component has the handleMessageSubmit function passed to its overrideSubmitHandler prop; enabling the MessageInput component to send the user-entered message to the chat and also to the backend.

 if (\!channel) {

    return (

      \<div className\="chat-widget chat-widget--error"\>

        Failed to load chat

      \</div\>

    );

  }

  return (

    \<div className\="chat-widget"\>

      \<Channel channel\={channel}\>

        \<Window\>

          \<ChannelHeader /\>

          \<MessageList messageLimit\={100} /\>

          \<MessageInput

            overrideSubmitHandler\={handleMessageSubmit}

          /\>

        \</Window\>

      \</Channel\>

    \</div\>

  );
  ```

### Run the Frontend

Enter this command to run the frontend:

```bash
npm run dev

Next Steps

Having completed the implementation of the feedback system with Stream React Chat SDK and Slack, you now have a streamlined system that collects user feedback in real-time and sends it directly to your team. This system makes feedback management easy and channels it in a single hub, ensuring it reaches decision-makers quickly.

What’s next? Keep building with these recommended tutorials:

Happy coding. 🎉

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->