Build a Slack bot with Node.js and Bolt in 7 steps — OAuth, event handling, deployment, and real code examples for solo founders.
Got two hours? That's typically enough to create a Slack bot if you know where the friction points are. Use Node.js, Slack's Bolt framework, and OAuth to authenticate. Then, deploy to a VPS or serverless function so your bot runs 24/7 without interruptions.
Photo: Ant Rozetsky on Unsplash
Who this is for: Solo founders managing small teams (one to five people) who want to automate Slack workflows. Think standup reminders, customer alerts, deployment notifications, all without paying for Zapier or hiring a backend developer.
Step 1: Create a Slack App and Configure Permissions
Start by visiting api.slack.com/apps and click "Create New App." Choose "From scratch," give your bot a name, and select your testing workspace.
Head over to OAuth & Permissions in the sidebar. Add these under "Bot Token Scopes":
chat:write— for sending messageschannels:history— to read public channel messagesapp_mentions:read— triggers when @mentionedcommands— enables slash commands
Install the app in your workspace. Slack will give you a Bot User OAuth Token, starting with xoxb-. Copy it for Step 3.
In Event Subscriptions, toggle "Enable Events" on. Save this spot for Step 5 to add a Request URL after your server is live.
Step 2: Set Up Your Node.js Project
Photo: Scott Webb on Unsplash
Set up a new directory and initialize npm:
mkdir slack-bot
cd slack-bot
npm init -y
Install Slack's Bolt framework and dotenv for environment variables:
npm install @slack/bolt dotenv
Create a .env file in your project root:
SLACK_BOT_TOKEN=xoxb-your-token-here
SLACK_SIGNING_SECRET=your-signing-secret
Get your signing secret from Slack app's Basic Information page under "App Credentials."
Create a .gitignore:
node_modules
.env
Step 3: Write the Bot Server Code
Develop app.js:
require('dotenv').config();
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: false, // We'll use HTTP mode for production
port: process.env.PORT || 3000
});
// Respond to app mentions
app.event('app_mention', async ({ event, client }) => {
try {
await client.chat.postMessage({
channel: event.channel,
text: `Hey <@${event.user}>, I'm alive and running on Node.js.`
});
} catch (error) {
console.error(error);
}
});
// Slash command example
app.command('/status', async ({ command, ack, respond }) => {
await ack();
await respond(`Server uptime: ${process.uptime()} seconds`);
});
// Message listener for specific keywords
app.message('deploy', async ({ message, say }) => {
await say({
text: `Deployment detected in <#${message.channel}>`,
thread_ts: message.ts
});
});
(async () => {
await app.start();
console.log('⚡️ Bolt app is running on port', process.env.PORT || 3000);
})();
This bot does three things:
- Responds to @mentions.
- Handles a
/statusslash command. - Listens for "deploy" messages and replies in-thread.
Test it locally:
node app.js
Right now, your bot won't receive events. Slack can't reach localhost. You'll need a public URL.
Step 4: Expose Your Local Server with ngrok
Install ngrok or the npm version:
npm install -g ngrok
ngrok http 3000
Ngrok supplies a public URL like https://abc123.ngrok.io. Copy the HTTPS one.
Backtrack to Slack app's Event Subscriptions page. Add your ngrok URL appended with /slack/events:
https://abc123.ngrok.io/slack/events
Slack sends a challenge request. If your server is active, it auto-responds and shows "Verified."
Under "Subscribe to bot events," add:
app_mentionmessage.channels
Click "Save Changes."
Navigate to Slash Commands and create a new one:
- Command:
/status - Request URL:
https://abc123.ngrok.io/slack/events - Short Description: "Check bot status"
Slack will prompt you to reinstall the app.
Now test in Slack. Type /status or @mention your bot. Responses should show up.
Step 5: Add Real Functionality — API Integration Example
Most bots pull data from external APIs. Here's how to fetch GitHub repo stars and send that data to Slack:
Install axios:
npm install axios
Insert this into app.js:
const axios = require('axios');
app.command('/github', async ({ command, ack, respond }) => {
await ack();
const repo = command.text; // e.g., "facebook/react"
if (!repo) {
return respond('Usage: `/github owner/repo`');
}
try {
const { data } = await axios.get(`https://api.github.com/repos/${repo}`);
await respond({
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${data.full_name}*\n⭐ ${data.stargazers_count} stars\n🍴 ${data.forks_count} forks\n${data.description || 'No description'}`
}
}
]
});
} catch (error) {
await respond('Repository not found or API error.');
}
});
Restart your bot, then run /github facebook/react in Slack. Expect a formatted card displaying repo stats.
Step 6: Deploy to Production
Ngrok suffices for testing but halts when the terminal is closed. Deploy to a VPS or serverless platform for continuous running.
Option A: Deploy to a $5 DigitalOcean Droplet
SSH into your server:
ssh root@your-server-ip
Install Node.js:
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
Clone your repo (or scp your files), install dependencies, and run with PM2:
npm install pm2 -g
pm2 start app.js --name slack-bot
pm2 startup
pm2 save
Point your domain or use the server IP. Update Slack's Request URL to http://your-server-ip:3000/slack/events.
Option B: Deploy to Vercel or Railway
Vercel doesn't support persistent WebSocket connections, but HTTP mode works. Create vercel.json:
{
"version": 2,
"builds": [{ "src": "app.js", "use": "@vercel/node" }],
"routes": [{ "src": "/(.*)", "dest": "/app.js" }]
}
Deploy:
npm i -g vercel
vercel --prod
Update Slack's Request URL to your Vercel domain.
Railway simplifies bot deployment — it supports long-running processes. Push your repo to GitHub, connect it to Railway, add environment variables in the dashboard, and deploy. Railway generates a public URL automatically.
Step 7: Secure Your Bot and Handle Rate Limits
Slack enforces rate limits — typically one request per second for most methods. If your bot sends bulk messages, use a queue:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function sendBulkMessages(channels, message) {
for (const channel of channels) {
await app.client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel,
text: message
});
await delay(1100); // Slack allows ~1 msg/sec
}
}
Verify requests. Bolt handles this by default, but ensure your signing secret is set.
Keep your bot token in environment variables, not in the code. If tokens end up on GitHub, immediately rotate them in your Slack app settings.
What Nobody Tells You About Slack Bots
Slack's Event API has a 3-second timeout. Calling a slow external API will cause errors. Use ack() right away, then handle responses asynchronously:
app.command('/slow-api', async ({ command, ack, respond }) => {
await ack(); // Acknowledge within 3 seconds
// Do slow work here
const result = await someSlowApiCall();
await respond(result);
});
Socket Mode exists but has caveats. Socket Mode lets you run bots without a public URL, useful for development. However, it needs a WebSocket connection, unsuitable for serverless platforms. HTTP mode is best for production unless using a VPS.
Bolt can obscure debugging. If events aren't firing, check Slack's Event Subscriptions for delivery errors. Failed requests and payloads are shown — way more helpful than console logs.
Slash commands don't natively support subcommands. Parse command.text manually. Split it into segments and route as needed:
app.command('/mybot', async ({ command, ack, respond }) => {
await ack();
const [action, ...args] = command.text.split(' ');
if (action === 'deploy') {
// handle deploy
} else if (action === 'status') {
// handle status
} else {
await respond('Unknown command. Try `/mybot deploy` or `/mybot status`.');
}
});
Slack's Block Kit is overkill until it's not. Start with plain text. Need buttons, modals, or rich formatting? Then, use Block Kit Builder. Copy JSON blocks directly into your code when required.
Common Mistakes
Installing the app without correct scopes. If your bot can't read messages, you likely missed channels:history. Can't post? You're missing chat:write. Always check "OAuth & Permissions" and reinstall after adding scopes.
Not handling message subtypes. Slack sends events for edits, deletions, and bot messages. Filter by message.subtype to avoid infinite loops:
app.message(async ({ message, say }) => {
if (message.subtype) return; // Ignore edits, bot messages, etc.
await say('Got a real user message');
});
Hardcoding channel IDs. Channel IDs can change if a channel is recreated. Use channel names or let users configure channels with slash commands.
Forgetting to rotate tokens. A leaked bot token allows anyone to impersonate your bot. Rotate tokens in Slack's "OAuth & Permissions" and update your .env right away.
FAQ
Do I need a paid Slack plan to build bots?
No. Bots work on Slack's free tier. You're limited to 10 integrations total, but a single bot counts as one, no matter its features. Paid plans unlock unlimited integrations and longer message history, but neither impacts bot functionality.
Can I use TypeScript instead of JavaScript?
Yes. Install @types/node and ts-node, rename app.js to app.ts, and add types for Bolt. The Slack SDK includes TypeScript definitions. Compile using tsc or run with ts-node app.ts. TypeScript enhances autocomplete and reduces runtime errors in most production bots.
How do I test my bot without spamming my team?
Create a private channel or separate Slack workspace for development. Invite only your bot. Test all events and commands there. Slack allows you to create up to 10 workspaces on the free plan. Use one just for bot testing.
What's the difference between Event API and RTM?
Event API uses HTTP requests. Slack sends events to your server's public URL. RTM (Real Time Messaging) uses WebSockets and maintains a persistent Slack connection. RTM is deprecated. Use Event API for new bots, or Socket Mode if a public URL can't be exposed.
Conclusion
The bottom line: you now have a functional Slack bot listening to mentions, handling slash commands, and fetching API data. Deploy it to a $5 VPS or Railway, then implement the workflows your team actually needs. Stop paying for automation tools that do the same thing.
Next step: Identify one manual task you perform daily in Slack, like standup reminders, deployment notifications, or customer alerts, and automate it. Write the handler in app.js, test in a private channel, and ship it today. For more insights on automating tasks, check out our article on Step-by-Step Mailchimp Setup for Solo Founders or explore Best CRM Tools for Indie Hackers in 2026.
Editorial note: This article was produced with AI assistance and reviewed by Javier Valencia. Verified facts are distinguished from editorial opinion throughout the text. External sources linked are independent of NewsTide.
Sources
More in Indie Hacking
🇪🇸 Also available in Spanish: Leer en español