Skip to content

Open Source Poll App for Slack, which anyone can deploy on their own

License

Notifications You must be signed in to change notification settings

profiq/slack-open-poll

Repository files navigation

πŸ—³οΈ Open Poll – TypeScript Open Source Slack Poll App

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.


πŸš€ Project Goals

  • 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

🧩 Key Features (Planned)

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

πŸ”§ Tech Stack

  • 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

πŸš€ Deploying Open Poll

🧠 First, ensure you have Node.js and Firebase CLI installed.

1. Clone the repository

git clone https://github.com/profiq/slack-open-poll.git
cd slack-open-poll

2. Create a .env file with your configuration

  • Create this file in /functions directory
SLACK_SIGNING_SECRET=your_signing_secret
SLACK_BOT_TOKEN=xoxb-your-bot-token
NODE_ENV=development
FIRESTORE_EMULATOR_HOST=localhost:8080

You are going to get SLACK_SIGNING_SECRET and SLACK_BOT_TOKEN in step number 3

3. First Slack App Configuration

  • 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 Secret and paste it into your .env
  • Go to Features/OAuth & Permissions/ and click Install to your-workspace
  • From the same page copy Bot User OAuth Token and paste it into your .env as SLACK_BOT_TOKEN

4. Set up Firebase

4.1 Firebase Cloud Functions deployment

  • 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 1 and enter

4.3 Monorepo dev shortcuts

  • 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
    • Web only:
      npm -w web run dev

4.2 Local Development with Firebase Emulator + ngrok tunnel

  • Go to the .env file and paste DEFAULT_FUNCTIONS_LOCATION=us-central1 or 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/slack you need this part slack-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

5. Finish Slack App Configuration

  • 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 with slack/eventsit should look like
      • https://europe-west3-your-project-name.cloudfunctions.net/slack/events
      • with ngrok should be likehttps://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!

Architecture

Flow chart

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
Loading

Architecture Diagram

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
Loading

Slash Command Handler

  • Handler processes /poll commands 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

Funcionality

  • 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

Flags

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"...

Usage and Example

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

Vote Handler

Handler processes app.action when the vote happens

Functionality

  • 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

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

Usage of Logger

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);
}

πŸ“š Learning Resources

If you're new to some parts of this stack, check out these:

🧠 Slack App Development

πŸ”₯ TypeScript & Node

🐳 Docker & DevOps


🀝 Contributing

We welcome contributions to Open Poll!

  1. Fork the repo
  2. Create a new feature branch
  3. Make your changes
  4. Submit a PR

Please follow the Conventional Commits style for commit messages.


πŸ“Œ Roadmap (Milestones)

  • ⬜ 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

πŸ“œ License

MIT – free to use, modify, and distribute.


Made with ❀️ by profiq.

About

Open Source Poll App for Slack, which anyone can deploy on their own

Topics

Resources

License

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •  

Languages