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)

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

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

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;

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

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}`,
  });
});
// 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}`);
  }
});

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

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