Summary
Every dispatched notification is recorded in wp_mam_notification_history. The in-app notification center (Open Notifications content class) reads from it. Per-user opt-out is enforced before send.
wp_mam_notification_history
Owned by MAM_Notification_History_Repository. Renamed from wp_tsl_local_app_notifications_history in PR #31. Frozen — do not rename in place.
Per-row schema (approximate):
| Column | Purpose |
|---|---|
id |
Primary key |
user_id |
Recipient WP user id |
message_type |
Slug from the type registry |
subject |
Title line |
message |
Body text |
channel |
email / sms / push / in_app |
status |
sent / failed / queued / read |
created_at |
Insert timestamp |
read_at |
Null until the user marks it read |
data |
Serialized extra payload (push deeplink, action button data, etc.) |
A single mam_notification_send_message fire that touches three channels produces three rows (one per channel).
In-app notification center
The Open Notifications content class reads via MAM_Notification_History_Repository::for_user($user_id, $limit) and renders rows ordered by created_at DESC. The unread_count is WHERE read_at IS NULL.
Marking-read on open issues:
UPDATE wp_mam_notification_history
SET read_at = NOW()
WHERE user_id = ? AND read_at IS NULL
This is a write that runs every time the user opens the notification screen — high-frequency open events have measurable DB load. Tracked.
Opt-out: email
Per-user opt-out is stored in wp_mam_email_opted_out (or per-user meta — varies by site). Before sending, MAM_Email_Sender checks:
if ( $this->is_opted_out( $user_id, $email ) ) {
// skip; record as opted_out in history
return;
}
Opt-outs are populated from:
- The unsubscribe link in outbound emails
- An admin override in Mobile App Manager → Notifications → Opt-Outs
- A per-user toggle in the user’s profile
Opt-out: SMS
SMS opt-out is provider-managed. Twilio responds to “STOP” replies by suppressing future messages from that number. mam-main appends the STOP-to-opt-out copy automatically; subsequent attempts to send to a STOPped number return a Twilio error.
mam-main does not maintain its own SMS opt-out table — Twilio is the source of truth.
Opt-out: push
Push has no opt-out table. The user opts out by:
- Revoking push permission in iOS / Android Settings (the OS stops delivering)
- Uninstalling the app (token becomes invalid)
- Explicitly disabling in-app push (sets
usermetapush_enabled = 0)
Send attempts to a revoked-permission token return BadDeviceToken from APNs / Unregistered from FCM. The token repository should drop the stored token on these errors (tracked as a hardening item).
Outbox viewer
Mobile App Manager → Notifications → Outbox uses mam_notifications_outbox_viewer (a WP_List_Table subclass) to surface recent dispatches across all channels with their statuses. Useful for diagnosing delivery failures.
Gotchas
- History rows are not deleted automatically. A high-traffic site accumulates rows indefinitely. If you implement pruning, do it via the repository — direct
DELETEon the table risks corrupting indexes used by the in-app notification fetch. - A failed channel still produces a history row. The
statuscolumn distinguishes; the in-app center should filter tostatus IN (sent, read)to hide failures. - Opt-out is checked at send time, not at queue time. A user who opts out between queue and send (cron tick later) has the opt-out honored.
- PR #31 rename: backward-compat aliases mean direct
$wpdbreads against the legacywp_tsl_local_app_notifications_historytable name no longer work. Use the repository.
Related articles
- Notifications overview
- Notification channels: email, SMS, push
- Notification queue and cron
- Content class: Open Notifications
- Frozen public contracts reference
Metadata
| Field | Value |
|---|---|
| Article type | Plugin Overview |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Plugin Reference |
| Audience | PHP developer |
| Last verified | 2026-05-02 |
