DEV Community

Cover image for Build a Social Media Event Bus: React to Posts, Comments, and Follows in Real-Time
Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Social Media Event Bus: React to Posts, Comments, and Follows in Real-Time

Social media platforms don't give you webhooks. Instagram won't ping your server when someone comments. TikTok won't notify you when a creator posts.

So you build your own.

I built an event bus that polls social media APIs and converts changes into events. New post? Event. New comment? Event. Follower count changed by more than 5%? Event. Then any downstream system can subscribe — Discord bots, email senders, dashboards, CRMs.

It turned 10 separate "check social media" scripts into one system.

Architecture

Poller (cron jobs)
  │
  ├── Check profiles every 30 minutes
  ├── Check posts every 15 minutes
  ├── Check comments every hour
  │
  ↓ Detect changes (diff against last known state)
  │
Event Bus (in-process EventEmitter or Redis Pub/Sub)
  │
  ├── → Discord notifier
  ├── → Email sender
  ├── → Database logger
  ├── → Slack alerter
  └── → Webhook forwarder (POST to any URL)
Enter fullscreen mode Exit fullscreen mode

The pollers detect changes. The event bus routes them. The handlers do whatever you want. Completely decoupled.

The Stack

  • Node.js – runtime
  • SociaVault API – data source
  • EventEmitter (built-in) – event bus for single-process; Redis Pub/Sub for multi-process
  • better-sqlite3 – state tracking
  • node-cron – polling schedule

Setup

mkdir social-event-bus && cd social-event-bus
npm init -y
npm install axios better-sqlite3 node-cron dotenv
Enter fullscreen mode Exit fullscreen mode

Step 1: The State Store

To detect changes, you need to know what things looked like last time you checked.

// state.js
const Database = require('better-sqlite3');
const db = new Database('./state.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS known_state (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

const getState = db.prepare('SELECT value FROM known_state WHERE key = ?');
const setState = db.prepare(`
  INSERT INTO known_state (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
  ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
`);

module.exports = {
  get: (key) => {
    const row = getState.get(key);
    return row ? JSON.parse(row.value) : null;
  },
  set: (key, value) => {
    setState.run(key, JSON.stringify(value));
  },
};
Enter fullscreen mode Exit fullscreen mode

Step 2: The Event Bus

// bus.js
const { EventEmitter } = require('events');

class SocialEventBus extends EventEmitter {
  emit(eventType, payload) {
    const event = {
      type: eventType,
      timestamp: new Date().toISOString(),
      ...payload,
    };

    // Log every event
    console.log(`[EVENT] ${eventType}${payload.platform}/@${payload.username || 'unknown'}`);

    // Emit both the specific event and a wildcard
    super.emit(eventType, event);
    super.emit('*', event);

    return true;
  }
}

// Singleton
const bus = new SocialEventBus();
module.exports = bus;
Enter fullscreen mode Exit fullscreen mode

Event types we'll generate:

Event Trigger
new_post Creator published a new post/video
post_milestone A post crossed a view/like threshold
follower_change Follower count changed significantly (±5%)
new_comment New comment on a tracked post
engagement_spike Post engagement rate is 3x+ above creator's average
profile_updated Bio, name, or profile pic changed

Step 3: The Pollers

Each poller fetches current data, diffs against stored state, and emits events for any changes.

// pollers/profile-poller.js
const axios = require('axios');
const state = require('../state');
const bus = require('../bus');

const api = axios.create({
  baseURL: 'https://api.sociavault.com/v1/scrape',
  headers: { 'x-api-key': process.env.SOCIAVAULT_API_KEY },
});

async function pollProfile(platform, username) {
  const endpoint = platform === 'instagram'
    ? `/instagram/profile?username=${username}`
    : `/tiktok/profile?username=${username}`;

  try {
    const { data: res } = await api.get(endpoint);
    const profile = res.data || res;

    const key = `profile:${platform}:${username}`;
    const previous = state.get(key);

    const current = {
      followers: profile.followersCount || profile.followerCount || 0,
      following: profile.followingCount || 0,
      posts: profile.postsCount || profile.videoCount || 0,
      bio: profile.bio || profile.signature || '',
      displayName: profile.fullName || profile.nickname || '',
    };

    if (previous) {
      // Check for follower changes (±5% or ±1000)
      const followerDelta = current.followers - previous.followers;
      const followerPercent = previous.followers > 0
        ? Math.abs(followerDelta / previous.followers) * 100
        : 0;

      if (followerPercent >= 5 || Math.abs(followerDelta) >= 1000) {
        bus.emit('follower_change', {
          platform,
          username,
          previous: previous.followers,
          current: current.followers,
          delta: followerDelta,
          percentChange: parseFloat(followerPercent.toFixed(1)),
        });
      }

      // Check for new posts
      if (current.posts > previous.posts) {
        bus.emit('new_post', {
          platform,
          username,
          previousCount: previous.posts,
          currentCount: current.posts,
          newPosts: current.posts - previous.posts,
        });
      }

      // Check for bio changes
      if (current.bio !== previous.bio) {
        bus.emit('profile_updated', {
          platform,
          username,
          field: 'bio',
          old: previous.bio,
          new: current.bio,
        });
      }
    }

    state.set(key, current);
  } catch (err) {
    console.error(`Poll failed for ${platform}/@${username}: ${err.message}`);
  }
}

module.exports = { pollProfile };
Enter fullscreen mode Exit fullscreen mode
// pollers/post-poller.js
const axios = require('axios');
const state = require('../state');
const bus = require('../bus');

const api = axios.create({
  baseURL: 'https://api.sociavault.com/v1/scrape',
  headers: { 'x-api-key': process.env.SOCIAVAULT_API_KEY },
});

async function pollPosts(platform, username) {
  const endpoint = platform === 'instagram'
    ? `/instagram/posts?username=${username}&limit=5`
    : `/tiktok/profile-videos?username=${username}&limit=5`;

  try {
    const { data: res } = await api.get(endpoint);
    const posts = res.data || res.posts || [];

    for (const post of posts) {
      const postId = post.id || post.shortcode || post.videoId;
      if (!postId) continue;

      const key = `post:${platform}:${postId}`;
      const previous = state.get(key);

      const current = {
        likes: post.likesCount || post.diggCount || 0,
        comments: post.commentsCount || post.commentCount || 0,
        views: post.viewCount || post.playCount || null,
        shares: post.shareCount || null,
      };

      if (previous) {
        // Check for engagement spike
        const likeGrowth = previous.likes > 0
          ? current.likes / previous.likes
          : 0;

        if (likeGrowth >= 3) {
          bus.emit('engagement_spike', {
            platform,
            username,
            postId,
            metric: 'likes',
            previous: previous.likes,
            current: current.likes,
            multiplier: parseFloat(likeGrowth.toFixed(1)),
          });
        }

        // Check for view milestones (10K, 100K, 1M)
        const milestones = [10000, 100000, 1000000, 10000000];
        if (current.views) {
          for (const milestone of milestones) {
            if (previous.views < milestone && current.views >= milestone) {
              bus.emit('post_milestone', {
                platform,
                username,
                postId,
                milestone,
                currentViews: current.views,
              });
            }
          }
        }
      }

      state.set(key, current);
    }
  } catch (err) {
    console.error(`Post poll failed for ${platform}/@${username}: ${err.message}`);
  }
}

module.exports = { pollPosts };
Enter fullscreen mode Exit fullscreen mode

Step 4: The Handlers

This is where you plug in whatever actions you want:

// handlers/discord.js
const axios = require('axios');
const bus = require('../bus');

const DISCORD_WEBHOOK = process.env.DISCORD_WEBHOOK_URL;

bus.on('new_post', async (event) => {
  if (!DISCORD_WEBHOOK) return;

  await axios.post(DISCORD_WEBHOOK, {
    content: `🆕 **@${event.username}** posted ${event.newPosts} new ${event.newPosts === 1 ? 'post' : 'posts'} on ${event.platform}!`,
  });
});

bus.on('engagement_spike', async (event) => {
  if (!DISCORD_WEBHOOK) return;

  await axios.post(DISCORD_WEBHOOK, {
    content: `🔥 **Engagement spike!** @${event.username}'s post is getting ${event.multiplier}x normal likes on ${event.platform}`,
  });
});

bus.on('follower_change', async (event) => {
  if (!DISCORD_WEBHOOK) return;

  const direction = event.delta > 0 ? '📈' : '📉';
  const sign = event.delta > 0 ? '+' : '';
  await axios.post(DISCORD_WEBHOOK, {
    content: `${direction} **@${event.username}** ${sign}${event.delta.toLocaleString()} followers (${event.percentChange}%) on ${event.platform}`,
  });
});
Enter fullscreen mode Exit fullscreen mode
// handlers/webhook-forwarder.js
const axios = require('axios');
const bus = require('../bus');

// Forward all events to an external URL (your own API, Zapier, n8n, etc.)
const WEBHOOK_URL = process.env.FORWARD_WEBHOOK_URL;

bus.on('*', async (event) => {
  if (!WEBHOOK_URL) return;

  try {
    await axios.post(WEBHOOK_URL, event, {
      headers: { 'Content-Type': 'application/json' },
      timeout: 5000,
    });
  } catch (err) {
    console.error(`Webhook forward failed: ${err.message}`);
  }
});
Enter fullscreen mode Exit fullscreen mode

Step 5: Main Entry Point

// index.js
require('dotenv').config();
const cron = require('node-cron');
const { pollProfile } = require('./pollers/profile-poller');
const { pollPosts } = require('./pollers/post-poller');

// Load handlers (they self-register on the bus)
require('./handlers/discord');
require('./handlers/webhook-forwarder');

// Accounts to monitor
const WATCHED = [
  { platform: 'instagram', username: 'competitor_1' },
  { platform: 'instagram', username: 'competitor_2' },
  { platform: 'tiktok', username: 'competitor_3' },
  { platform: 'tiktok', username: 'your_own_account' },
];

async function runProfilePolls() {
  console.log(`[${new Date().toISOString()}] Polling profiles...`);
  for (const account of WATCHED) {
    await pollProfile(account.platform, account.username);
    await new Promise(r => setTimeout(r, 500));
  }
}

async function runPostPolls() {
  console.log(`[${new Date().toISOString()}] Polling posts...`);
  for (const account of WATCHED) {
    await pollPosts(account.platform, account.username);
    await new Promise(r => setTimeout(r, 500));
  }
}

// Initial run
runProfilePolls();
runPostPolls();

// Schedule
cron.schedule('*/30 * * * *', runProfilePolls);  // Profiles every 30 min
cron.schedule('*/15 * * * *', runPostPolls);      // Posts every 15 min

console.log(`Social event bus started. Watching ${WATCHED.length} accounts.`);
console.log('Profile polls: every 30 minutes');
console.log('Post polls: every 15 minutes');
Enter fullscreen mode Exit fullscreen mode

Why This Pattern?

Because polling scripts always start simple and end up as spaghetti. You start with one script that checks competitors and sends a Discord message. Then your boss wants Slack too. Then email. Then someone wants to log it to a spreadsheet. Then you need to check comments too, not just posts.

The event bus pattern means:

  • Adding a new data source = write one poller function
  • Adding a new action = write one handler function
  • They don't know about each other — the poller doesn't care if Discord or Slack or email is listening

I've run this pattern for 6 months. Added 4 handlers and 2 pollers without touching existing code once.

Scaling Up

When you outgrow a single Node.js process:

  1. Replace EventEmitter with Redis Pub/Sub — pollers publish, handlers subscribe, can run on different machines
  2. Move pollers to separate workers — one per platform
  3. Add a dead letter queue for failed handler deliveries
  4. Add a simple web UI to see recent events (Express + SSE)

But honestly, a single Node process on a $5 VPS handles 50+ accounts with room to spare.

Read the Full Guide

Build a Social Media Event Bus → SociaVault Blog


Turn social media data into real-time events with SociaVault — one API for TikTok, Instagram, YouTube, and 10+ platforms. Profiles, posts, comments, followers — all endpoints, one key.

Discussion

What's your approach to "real-time" social media monitoring when the platforms don't offer webhooks? Poll and diff like this, or a different strategy entirely?

javascript #nodejs #architecture #webdev #api

Top comments (0)