WP Manifestindependent plugin directory
manifest / email / wp-notifications-hub

Notifications Hub

Enterprise WordPress notifications plugin with custom database tables, REST API endpoints, unread indicators, and scheduled email digests via corporate SMTP

by redrofigt · github.com/redrofigt/wp-notifications-hub · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/redrofigt/wp-notifications-hub/archive/refs/heads/main.zip

Readme

Notifications Hub for WordPress

Enterprise notifications plugin for WordPress intranets: a dedicated custom database table, REST API endpoints, unread indicators, per-type badge counts, and scheduled email digests through corporate SMTP.

Built to WordPress coding standards: prepared statements everywhere, capability checks, whitelisted query inputs, and index-backed queries designed for 100k+ notification rows.

Why a custom table (not post meta)

Concern wp_postmeta wp_nh_notifications
Composite index for inbox queries ❌ not possible user_inbox (user_id, is_read, created_at)
Unread badge aggregate ❌ full scan via meta_query ✅ covering-index GROUP BY
Idempotent webhook inserts dedupe_idx unique-style guard
Retention cleanup ❌ slow ✅ indexed DELETE on expires_at, daily cron

Full rationale, schema, and the non-trivial query (with EXPLAIN output) are documented in docs/non-trivial-query.md.

Features

  • Custom table created via dbDelta() with 4 purpose-built indexes
  • REST API under notifications-hub/v1: GET /inbox, GET /unread-summary, POST /mark-read
    • every route requires authentication and is scoped to the authenticated user — client-supplied user_id is never trusted
  • Unread bell shortcode [nh_unread_bell] with a 60-second background refresher and 99+ capping
  • Hourly digest cron batching users 100 at a time, sending only to opted-in users via wp_mail() (corporate SMTP)
  • De-duplication on user_id + type + source + source_id so retried HR/SharePoint webhooks never double-notify
  • Daily retention purge of expired / 90-day-old rows

Schema (summary)

CREATE TABLE wp_nh_notifications (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  type VARCHAR(32) NOT NULL DEFAULT 'general',
  title VARCHAR(191) NOT NULL,
  body TEXT NULL,
  link VARCHAR(255) NULL,
  source VARCHAR(64) NOT NULL DEFAULT 'system',
  source_id BIGINT UNSIGNED NULL,
  is_read TINYINT(1) NOT NULL DEFAULT 0,
  read_at DATETIME NULL,
  emailed TINYINT(1) NOT NULL DEFAULT 0,
  expires_at DATETIME NULL,
  created_at DATETIME NOT NULL,
  PRIMARY KEY (id),
  KEY user_inbox (user_id, is_read, created_at),
  KEY type_created (type, created_at),
  KEY cleanup_idx (expires_at),
  KEY dedupe_idx (user_id, type, source, source_id)
);

Usage (PHP)

use NotificationsHub\Store;

// Notify one employee (de-duplicated by source_id).
Store::push( array(
    'user_id'   => 42,
    'type'      => 'policy',
    'title'     => 'New leave policy published',
    'link'      => 'https://intranet.example.com/policies/leave-2026',
    'source'    => 'sharepoint',
    'source_id' => 99123,
) );

// Broadcast to a department (chunked inserts).
Store::broadcast( $department_user_ids, array(
    'type'  => 'event',
    'title' => 'Town hall moved to 14:00',
) );

// Badge data: total + per-type counts in one query.
$summary = Store::get_unread_summary( get_current_user_id() );
// [ 'total' => 7, 'by_type' => [ 'news' => 4, 'hr' => 2, 'policy' => 1 ] ]

Security notes

  • All SQL through $wpdb->prepare(); IN() lists are built from absint-ed values with whitelisted placeholders
  • Notification types are whitelisted before ever reaching SQL
  • mark_read enforces ownership in the UPDATE's WHERE clause (user_id = %d) — IDOR-safe by construction
  • REST responses pass rows through a whitelist hydrator; no raw DB columns leak

Requirements

  • WordPress 6.4+, PHP 8.1+
  • Any SMTP plugin (or mu-plugin) pointing wp_mail() at the corporate relay

Read the full README on GitHub →