Open Poll is an open-source Slack-integrated app that lets your team quickly create and run polls using a /poll command. Inspired by Simple Poll, Open Poll is built in TypeScript, runs on Firebase Cloud Functions, and is fully customizable for self-hosting or development learning.
The idea is that you self-host this application in your own Firebase instance. See the installation instructions bellow.
- Build a Slack-integrated polling bot
- Written in TypeScript
- Containerized with Docker
- Encourage learning and contributions via open-source collaboration
- Provide a lightweight, customizable alternative to commercial polling apps
| Feature | Status |
|---|---|
/poll Slack command |
β Planned |
| Anonymous & named voting | β Planned |
| Single & multi-choice polls | β Planned |
| Real-time vote updates | β Planned |
| Result summaries in thread | β Planned |
| Scheduled expiration of polls | β¬ Future |
| Web dashboard (admin optional) | β¬ Future |
- TypeScript β Strong typing for clean and scalable code
- Firebase Cloud Functions β Serverless backend API
- Cloud Firestore β Real-time poll & vote persistence
- Slack Bolt SDK β Slack integration & command handling
- ngrok / localtunnel β For local Slack command testing
π§ First, ensure you have Node.js and Firebase CLI installed.
git clone https://github.com/profiq/slack-open-poll.git
cd slack-open-poll- Create this file in
/functionsdirectory
SLACK_SIGNING_SECRET=your_signing_secret
SLACK_BOT_TOKEN=xoxb-your-bot-token
NODE_ENV=development
FIRESTORE_EMULATOR_HOST=localhost:8080You are going to get SLACK_SIGNING_SECRET and SLACK_BOT_TOKEN in step number 3
- Go to Slack Apps
- Create new app from a manifest
- Select your workspace
- Use this JSON:
{
"display_information": {
"name": "Open Poll"
},
"features": {
"bot_user": {
"display_name": "Open Poll",
"always_online": false
}
},
"oauth_config": {
"scopes": {
"bot": [
"channels:history",
"chat:write",
"commands",
"groups:history"
]
}
},
"settings": {
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}- From the page you see copy
Signing Secretand paste it into your .env - Go to Features/OAuth & Permissions/ and click
Install to your-workspace - From the same page copy
Bot User OAuth Tokenand paste it into your .env asSLACK_BOT_TOKEN
- Go to Firebase Console
- Create new project
- On the left panel at the bottom upgrade pricing plan to Pay as you go(you will not have to pay for anything)
- Go to Firestore Settings and create new default database, ideally in the same region you have your Functions set in .env file
- Now in terminal:
npm install -g firebase-tools
firebase login
cd functions
npm install
firebase use --add- Choose the Firebase project you just created
- Create alias for this project
- If you want to change the default deploy project:
firebase use
firebase use project-you-want-to-use- Now deploy
firebase deploy --only functions- Then it will ask you about clean up policy - just type
1and enter
- Start web dev server and all emulators together (from repo root):
npm run dev- Start only emulators (builds functions first):
npm run emulators- Start emulators without rebuilding functions:
npm run emulators:only- Run apps in isolation (handy for targeted development):
- Functions only:
- Build once:
npm run build:functions
- Watch/build in functions only (from functions/):
cd functions npm run build:watch - Start only functions + firestore emulators (classic):
firebase emulators:start --only functions,firestore
- Build once:
- Web only:
npm -w web run dev
- Functions only:
- Go to the
.envfile and pasteDEFAULT_FUNCTIONS_LOCATION=us-central1or insert another locations - Now in terminal:
npm install -g firebase-tools
firebase login
cd functions/
npm install
firebase use --add- In options select
slack-open-poll - Create alias for this project
- If you want to change the default deploy project:
firebase use
firebase use project-you-want-to-use- Now run emulators (from the repository root)
npm run emulators- Or, if functions are already built and you just want to restart emulators:
npm run emulators:only- Alternatively, using Firebase CLI directly:
firebase emulators:start --only auth,functions,firestore- In terminal find something like
β functions: Loaded functions definitions from source: slack.
β functions[us-central1-slack]: http function initialized (http://127.0.0.1:5001/slack-open-poll/us-central1/slack).- From
http://127.0.0.1:5001/slack-open-poll/us-central1/slackyou need this partslack-open-poll/us-central1/slack - Install and log in to ngrok
- Free plan should be enough
- In terminal
ngrok http 5001- In terminal you will see something like
Forwarding https://5b42e2aa8f30.ngrok-free.app -> http://localhost:5001- Copy generated public address for example
https://5b42e2aa8f30.ngrok-free.app
- In your Firebase Console project open
Build/Functions - You should see slack function and under Trigger and Request there is URL
- Copy this and use in your Slack App:
- Open Features/App Manifest
- paste this into JSON:
{ "display_information": { "name": "Open Poll" }, "features": { "bot_user": { "display_name": "Open Poll", "always_online": false }, "slash_commands": [ { "command": "/poll", "url": "INSERT-URL", "description": "Creates a poll!", "usage_hint": "\"question\", answer, answer OR help", "should_escape": false } ] }, "oauth_config": { "scopes": { "bot": [ "channels:history", "chat:write", "commands", "groups:history" ] } }, "settings": { "event_subscriptions": { "request_url": "INSERT-URL", "bot_events": [ "message.channels", "message.groups" ] }, "interactivity": { "is_enabled": true, "request_url": "INSERT-URL" }, "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": false } }- Paste your URL from Firebase Console Functions into fields with
INSERT-URL, make sure it ends withslack/eventsit should look likehttps://europe-west3-your-project-name.cloudfunctions.net/slack/events- with ngrok should be like
https://5b42e2aa8f30.ngrok-free.app/slack-open-poll/us-central1/slack/events
- Click Save Changes
- Now go to Features/OAuth & Permissions and click
Reinstal to your-workspace - And you are done!
flowchart TD
subgraph subGraph0["Firebase Function"]
D["Slack Bolt App with ExpressReceiver"]
C["Firebase Function"]
E["Handle Slash Command or Interaction"]
F["Read/Write to Firestore"]
G["Recalculate Vote Totals"]
H["Build Updated Slack Message"]
I["Call chat.update to refresh Slack message"]
end
A["User on Slack"] -- Slash Command or Button Click --> B["Slack API"]
B -- HTTP Request --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
sequenceDiagram
actor User as User
participant Slack as Slack API
participant FirebaseFunction as FirebaseFunction
participant SlackBolt as SlackBolt
participant Firestore as Firestore
User ->> Slack: /createpoll or clicks button
Slack ->> FirebaseFunction: Sends payload (slash command / interaction)
FirebaseFunction ->> SlackBolt: Passes request to ExpressReceiver
SlackBolt ->> SlackBolt: Parses command or interaction
SlackBolt ->> Firestore: Store poll or update vote
Firestore -->> SlackBolt: Poll data and vote results
SlackBolt ->> Slack: chat.postMessage or chat.update with new blocks
Slack -->> User: Updated poll message with current results
- Handler processes
/pollcommands from users, validates the input, creates a poll using chat.postMessage - Returns the poll message with options and buttons for voting
- Added functionality for multi-choice polls
- Parses poll question and options from command text
- Adds a lot of functionality by using flags
- Creates a poll using the PollService
- Returns a block message in the Slack channel with the question, options and buttons
- Handles errors if the command format is invalid or if the poll creation fails
- When errors happen, the slash command is responded by chat.postEphemeral which is shown just to the user that tried to create the poll
You can use flags before typing the question to customise the poll usage:
multiple:- creates a poll that enables having multiple options
-
/poll multiple "Question"...
limit:- upgraded multiple poll that enables maximum of votes given
-
/poll limit 2 "Question"... - creates a poll that enables maximum of 2 votes
- when trying to add vote above limit responds with ephemeral message
custom:- creates a poll that enables users to add custom option
- creates a button at the bottom of the poll
- when clicked it opens a form where you can type your new option and submit it
-
/poll custom "Question"...
anonymous:- creates a poll that does not show votes publicly (just the amount of votes)
- you can still see your votes in the "Open" form
-
/poll anonymous "Question"...
Tip: you can combine flags
/poll custom limit 2 "Question"...
Template:
/poll "Question?" Option 1, Option 2, Option 3
Example:
/poll "When would be the right time for a meeting?" 10 AM, 2 PM, 4 PM, 6 PM
Handler processes app.action when the vote happens
- The vote is recorded in the PollService
- After vote is recorded, the poll display is updated with the latest vote counts using user's ID
- If user votes again for the same option, the vote is deleted
If user votes again for different option:
- For single choice polls (default):
- Previous choice gets deleted
- New choice is added
- For multi-choice polls:
- Previous choices stay intact
- New choice is added to list of other choices
Open button in every Slack poll opens a form with poll options
There are two buttons:
Your Votes:- Shows your own options, which you have voted for
Settings: Only the creator of the poll can access this form Opens two buttons:Close Poll:- Closes the poll (poll.closed)
- Blocks another votes after the poll is closed
- Posts result in the poll message thread
- Updates original poll message with
Voting has ended for this poll - For any other vote attempt responds with ephemeral message
Delete Poll:- Soft deletes the poll in Firestore Database - just has the flag
deleted - Deletes the original poll message
- Displays informative form for user about deleting the poll
- Soft deletes the poll in Firestore Database - just has the flag
Basic setup:
import { Logger } from './utils/logger';
const logger = new Logger({ requestId: 'r-123' });
logger.info('Logger started');User context:
loger.info('User did something', {
userId: 'u-id',
workspaceId: 'w-id',
});Using withContext:
const userLogger = logger.withContext({
userId: 'u-id',
workspaceId: 'w-id',
});
userLogger.debug('Debugging user data');
userLogger.info('User joined a poll', { pollIdd: 'p-id' });Using logger errors:
try {
throw new Error('Error');
} catch (err) {
logger.error(err);
}If you're new to some parts of this stack, check out these:
We welcome contributions to Open Poll!
- Fork the repo
- Create a new feature branch
- Make your changes
- Submit a PR
Please follow the Conventional Commits style for commit messages.
- β¬ Slack command parsing & payload verification
- β¬ Docker dev environment
- β¬ Poll creation & vote storage (MongoDB)
- β¬ Vote interaction UI
- β¬ Web admin dashboard
- β¬ OAuth installation flow for multi-workspace support
MIT β free to use, modify, and distribute.
Made with β€οΈ by profiq.