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
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.zipReadme
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_idis never trusted
- every route requires authentication and is scoped to the authenticated user — client-supplied
- Unread bell shortcode
[nh_unread_bell]with a 60-second background refresher and99+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_idso 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 fromabsint-ed values with whitelisted placeholders - Notification types are whitelisted before ever reaching SQL
mark_readenforces 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