Architecture Overview
This is a technical map of how Super Speedy Emails (SSE) is built — the components, what each is for, and the design decisions worth knowing about. It is aimed at developers extending or debugging the plugin (and at an AI assistant handed the codebase). End-user how-tos live in the other KB articles; this is the engineering view.
Table of Contents
Component summary
| Component | File(s) | Purpose |
|---|---|---|
| Send pipeline | class-sse-campaigns.php (sse_send()), class-sse-email-log.php, class-sse-email-queue.php, class-sse-sender.php | The spine. Everything that sends an email funnels through it: log row → queue → cron drainer → provider. |
| Suppression gate | class-sse-suppression-gate.php, class-sse-suppressions.php | The single chokepoint that decides “may we send this category to this address?” Checked at enqueue and at send time. |
| Provider abstraction | class-sse-provider.php, class-sse-providers.php, providers/*, class-sse-webhook-dispatcher.php | Pluggable sending back-ends. The sender is provider-agnostic; providers only add tracking headers + parse their webhooks. |
| Segment engine | class-sse-segment.php | Evaluates a JSON predicate tree to “who matches this audience,” with automatic suppression masking. Single source of truth for audiences. |
| Newsletter | class-sse-newsletter.php | Post-publish instant alerts + scheduled digest, with per-subscriber category prefs and frequency. |
| Automations | class-sse-automations.php, class-sse-automation-scheduler.php | Triggered, time-offset email sequences (e.g. post-purchase drips). Enroll → tick → send. |
| Support emails | class-sse-support-emails.php, class-sse-support-rules.php, class-sse-support-context.php | Filter-driven product-update emails: per-record product checklist, fan-out per product, per-day guard. Engine is generic; the changelog reader is a separate companion plugin (ssp-changelog-emails). |
| Templates | class-sse-templates.php | Editable plain-text email templates with {{token}} substitution and two-pass rendering. |
| Forms & capture | class-sse-forms.php, class-sse-ajax.php, class-sse-rest.php, class-sse-shortcodes.php, class-sse-popup.php, blocks/signup-form/ | Signup forms (block/shortcode/popup), the REST + AJAX capture endpoints, spam controls. |
| Ultra AJAX fast path | mu-plugins/sse-fast-ajax.php, class-sse-mu-installer.php | A must-use plugin that handles signup submissions before the full WP stack loads — the “speed” differentiator. |
| Product ownership | class-sse-product-owners.php, class-sse-bundles.php | WooCommerce order → cache of “who owns which product,” including bundle expansion. |
| Preference centre | templates/manage-subscriptions.php, class-sse-woocommerce.php, class-sse-unsubscribe.php | Where subscribers self-manage: shortcode, WooCommerce My Account tab, and the tokenised /sse/u/ unsubscribe handler. |
| Migration | class-sse-migrate.php, class-sse-migrate-mailpoet.php, class-sse-migrate-mailerlite.php | One-way importers (MailPoet, MailerLite, WooCommerce backfill) via WP-CLI. |
| Rate limiting | class-sse-rate-limit.php | Per-IP throttle for the public capture/preference/unsubscribe endpoints. |
| Lifecycle | class-sse-activator.php, uninstall.php, defines.php, super-speedy-emails.php | Schema, cron registration, version upgrades, boot order, constants. |
Interesting / unusual aspects (read these first)
A few design choices set SSE apart from a typical WordPress email plugin and are worth understanding before diving in:
- One send funnel, gated twice. Nothing sends directly. Every path — campaigns, newsletter, automations, support emails, the public
sse_send()API — creates a log row, enqueues it, and lets the cron drainer do the actual send. The suppression gate is checked both at enqueue and again at send time, so an unsubscribe between queueing and delivery is still honoured. This makes “did we send it, and were we allowed to?” auditable in one place (sse_email_log).
- The queue is a real locking work-queue, not a loop. The drainer claims a batch with an atomic
UPDATE ... SET locked_by(worker token + lock expiry), so multiple overlapping cron runs never double-send. Failures retry with exponential backoff (1m, 4m, 16m, 64m, 256m) up to 5 attempts. This is closer to a job runner than the “fetch rows and email them” loop most plugins use.
- Audiences are a JSON predicate tree, not hard-coded queries.
SSE_Segmentevaluates a small set-algebra language (all_of/any_of/notover leaf predicates likeowns_product,purchased_in_last_days,prefers_category). Each predicate returns subscriber IDs; combinators set-op them. The top-level resolver automatically subtracts the category-appropriate suppressions, so a caller physically cannot forget the opt-out gate.
- Support emails are filter-driven, with the engine knowing nothing about changelogs. The engine asks two questions via filters — should this send? and what does it say? — and a separate bundled class answers them by reading changelogs. Swap the filter implementation and the same engine sends anything. (See the developer guide.)
- The “Ultra AJAX” mu-plugin bypasses full WordPress. Signup submissions are intercepted by a must-use plugin that loads before the plugin/theme stack and short-circuits the two visitor-facing routes (
subscribe,update-subscriptions) with a minimal WP load. That’s the speed claim, and it’s why those handlers live in a plain class (SSE_Ajax) callable from three contexts (mu-plugin, REST, admin-ajax) rather than being tied to the REST framework.
- Separate opt-out scopes, not one “unsubscribe” flag. Suppression is an
(email, scope)store with arbitrary scope strings (global,marketing,product_support,category:<id>,list:<id>,product_marketing:<id>). A marketing unsubscribe does not silence a product-update email, and vice-versa — encoded in the gate, not bolted on.
- Identity for self-service is possessed-credential only. The preference centre identifies a subscriber from the logged-in session or their own server-issued cookie — never a typed-in email — while logged-out users act through a 128-bit tokenised link. (This was a deliberate hardening.)
Boot order & lifecycle
super-speedy-emails.php is the bootstrap. Load order matters in a few places:
defines.phpfirst — provider visibility constants (SSE_PROVIDERS_ALL/TESTED/VISIBLE_MODE) must exist before the registry boots.- Core classes are
require_once‘d top to bottom; the send pipeline (Phase 2) loads before providers, andclass-sse-rest.phploads after the webhook dispatcher so its routes can register. - Providers are registered into
SSE_Providersat boot (all of them), but webhook routes are only registered for the tested + active providers (a security gate — see Provider abstraction below). - Singletons init on hooks:
SSE_Email_Queue::init(),SSE_Newsletter::init(),SSE_Automation_Scheduler::init(),SSE_Unsubscribe::init(),SSE_Support_Emails::init()at load;SSE_Shortcodes/Cookie/RESToninit; WooCommerce integration only whenclass_exists('WooCommerce').
Activation (SSE_Activator::activate()) creates all tables via dbDelta, schedules cron, flushes rewrite rules, and installs the mu-plugin. maybe_upgrade() runs on admin_init and re-creates tables + reschedules cron whenever the stored sse_db_version is behind the DB_VERSION constant — so schema/cron changes ship without a manual reactivate.
Database tables (15)
sse_lists, sse_subscribers, sse_subscriber_lists, sse_forms, sse_suppressions, sse_newsletter_prefs, sse_campaigns, sse_automations, sse_automation_steps, sse_automation_enrollments, sse_email_log, sse_email_events, sse_email_queue, sse_product_owners, sse_confirmation_tokens.
Cron events (4)
| Hook | Schedule | Does |
|---|---|---|
sse_drain_email_queue | every minute (sse_every_minute, a custom schedule) | drains the send queue |
sse_automation_tick | every minute | sends due automation steps |
sse_newsletter_digest_tick | hourly | fires the digest when a scheduled slot matches |
sse_support_email_check | daily | optional support-email run (off by default) |
Component architecture
Send pipeline (the spine)
Three stages, deliberately decoupled:
- Enqueue. A caller builds an
sse_email_logrow (recipient, category, subject, a JSONcontextblob holding body/tags/unsubscribe URL, and a legal basis) and callsSSE_Email_Queue::enqueue($log_id, $priority). The publicsse_send()function (inclass-sse-campaigns.php) is the documented entry point for other plugins — it runs the gate, ensures a subscriber row exists for transactional categories, writes the log, and enqueues, returning the log ID or aWP_Error.
- Drain.
SSE_Email_Queue::drain()runs every minute. It atomically claims up to 50 rows (locked_by= a random worker token,locked_until= now + 300s), ordered by priority then age, skipping rows still in backoff or past max attempts. For each claimed row it re-checks the suppression gate (state may have changed since enqueue), rebuilds send args from the logcontext, and calls the sender.
- Send + outcome. On success the log row goes to
sent(webhooks later progress it todelivered/opened/etc.) and the queue row is deleted. On aWP_Errorit incrementsattemptsand reschedules with exponential backoff, or marks the logfailedafter 5 tries. A suppressed-at-send row is markedsuppressedand dropped.
SSE_Sender::send() is the thin wrapper around wp_mail(): it applies the per-category sender identity (sse_from_name__<category> etc.) via temporary wp_mail_from* filters, injects the List-Unsubscribe header, converts plain text to simple linked HTML, and lets the active provider attach its tracking headers through phpmailer_init. Test modes (live / log_only / redirect) are enforced here — log_only skips the wp_mail() call entirely and returns a synthetic success.
Suppression gate (single chokepoint)
SSE_Suppression_Gate::check($email, $category, $context) returns {allowed, reason}. The rules are a matrix keyed on category:
globalblocks everything (hard bounce, spam complaint, full opt-out).transactional/onboarding— global only (service mail).product_support— global or the dedicatedproduct_supportscope (NOT marketing).- marketing-class (
newsletter,list_broadcast,product_marketing) — global,marketing, and their specific scope (category:<term>,list:<id>,product_marketing:<id>).
SSE_Suppressions is the backing store: a generic (email, scope, reason, source) table with add/remove/is_suppressed. Because scope is an arbitrary string, new opt-out dimensions need no schema change — product_support was added as just a new scope value + one gate branch.
Provider abstraction
SSE_Provider is the contract: identity (slug, label), header injection (inject_headers), and webhook handling (has_webhook, webhook_route, verify_webhook, parse_webhook_events). Concrete providers live in providers/ (none/wp_mail + Mailgun are tested; others are stubs).
SSE_Providers is the registry. Note two distinct sets: all() (every registered provider, used by the sender to find the active one) and webhook_providers() (the tested set plus the currently-selected provider — the only ones whose public webhook route is registered). This split is a security control: an unauthenticated webhook for a provider with stub verification would let anyone forge suppression events, so untested providers expose no endpoint.
SSE_Webhook_Dispatcher is the uniform persistence half. Every provider’s route points at it; it calls verify_webhook() (reject with 401 if it fails), then for each parsed event records it to sse_email_events, progresses the sse_email_log row, and auto-suppresses on complaint (→ marketing), hard bounce (→ global), or unsubscribe (→ the event’s scope). Adding a provider means writing only the parse half.
Segment engine
SSE_Segment::resolve($query, $category) is the audience resolver. The predicate vocabulary (documented in the file header) covers list membership, newsletter category preference, WooCommerce ownership (with bundle expansion and direct-only options), purchase recency/windows, product/subscriber/order meta-date ranges, attributes, and suppression membership — combined with all_of/any_of/not. Each leaf returns an array of subscriber IDs; combinators perform set intersection/union/difference. resolve() then applies the category-appropriate suppression mask; resolve_raw() skips it for diagnostics. This is what campaigns and the support-email engine use to answer “who?”.
SSE_Newsletter hooks transition_post_status for instant alerts (a post entering publish emails subscribers on the instant frequency whose category prefs match) and runs a digest on an hourly tick that fires only when the GMT day/hour matches a configured slot, gathering posts published since the last run. Per-subscriber state lives in two places: the digest_frequency column (instant|digest) and the sse_newsletter_prefs table (per category, default-subscribed when no explicit row). Bodies render through the newsletter_instant / newsletter_digest / newsletter_digest_item templates.
Automations
SSE_Automation_Scheduler implements triggered drips on the same enroll→tick→send shape:
- Enroll — a trigger (currently
wc_product_purchased, extensible via thesse_automation_triggersfilter) creates ansse_automation_enrollmentsrow, deduplicated by a context hash (e.g. order ID) so the same order can’t enrol twice. - Tick — the 1-minute
sse_automation_tickfinds enrollments whose next step is due (stepoffset_days/offset_hourscomputed from the enrol time), sends that step viasse_send(), and advances to the next step (or completes). - Cancel —
woocommerce_order_refundedcancels enrollments tied to that order.
Steps live in sse_automation_steps (ordered, each with its own offset and subject/body).
Support emails
Covered in depth in the developer guide. Architecturally: a Support Email is a record in sse_support_rules (name + product_ids checklist + recency rules + subject/body). SSE_Support_Emails is the generic engine — plan_support_email() / run_support_email() fan out over the record’s products, and for each product resolve owners via SSE_Product_Owners, apply the per-day guard (sse_email_log.source_reference = support:<record_id>:<product_id>, “created today”), the suppression gate and the recency rules, then queue. It exposes four filters — sse_should_send_support_email, sse_support_email_subject, sse_support_email_content, sse_support_email_should_send_to_user — each receiving an SSE_Support_Context, plus the sse_support_email_sent action. The core ships no changelog code; the changelog behaviour lives in a separate companion plugin (ssp-changelog-emails) that hooks those filters, reads readme.txt + a metadata JSON from the local filesystem (never HTTP), and maps product IDs to plugin slugs via sse_changelog_product_map. The per-product AJAX Preview/Send now buttons and the optional daily cron drive the same run_support_email().
Templates
SSE_Templates is a small registry (filterable via sse_templates_registry) of plain-text templates, each with a default subject/body and a token list. Storage is per-template options (sse_tpl_<key>_subject/_body); empty falls back to the default. Rendering is two-pass: render_string() replaces only the tokens it’s given and leaves the rest intact, so one party (e.g. the changelog class) can fill product tokens and the send engine later fills per-recipient tokens like {{first_name}}.
Forms, capture & the Ultra AJAX fast path
Forms are configured in SSE_Forms (fields, style preset, lists) and rendered inline (shortcode/Gutenberg block) or as a popup (SSE_Popup, triggered by delay/scroll). Submission posts to the REST routes in class-sse-rest.php, but the handlers themselves live in SSE_Ajax as plain static methods so they can be invoked from three places: the REST callback, admin-ajax, and the fast-ajax mu-plugin.
mu-plugins/sse-fast-ajax.php (installed by SSE_MU_Installer on activation) loads as a must-use plugin — before the normal plugin/theme stack. It inspects the request path, and for the two visitor-facing routes (subscribe, update-subscriptions) it short-circuits into a minimal WP load and calls SSE_Ajax directly, skipping the overhead of the full REST framework and the rest of the plugin/theme boot. Admin and other routes fall through to normal WordPress. This is the “near-instant signup” claim; it’s toggleable via the sse_options['sse_mu_enabled'] flag.
Spam controls: a honeypot field, the per-IP SSE_Rate_Limit, nonce verification, and email validation. Double opt-in is handled by SSE_Confirmations (256-bit single-use tokens in sse_confirmation_tokens, TTL filterable).
Product ownership
SSE_Product_Owners listens for WooCommerce orders reaching a paid status and upserts sse_product_owners rows: a direct=1 row for each purchased product, plus direct=0 rows for every component of a bundle (expanded via SSE_Bundles). subscriber_ids_owning($product_id, $include_bundles) is the lookup the support-email and segment engines use. Variations collapse to the parent product (WooCommerce reports the parent ID for line items), so a plugin sold in several licence tiers is one product for ownership purposes. A nightly rebuild keeps the cache honest.
Preference centre & unsubscribe
Three surfaces share Please log in to manage your email preferences.
If you have an account, log in here. Otherwise, you can unsubscribe at any time using the link at the bottom of any email we send you. templates/manage-subscriptions.php: the
shortcode, the WooCommerce “Email Preferences” My Account tab (SSE_WooCommerce, gated on a setting), and — separately — the /sse/u/ tokenised unsubscribe handler (SSE_Unsubscribe). The first two require a possessed identity (session or own cookie) and create a subscriber record on first save for a logged-in non-subscriber; the third needs only the 128-bit token and is the logged-out path. The handler honours RFC 8058 one-click POST and maps the link’s s=<scope> to a suppression scope.
Migration
SSE_Migrate is shared infrastructure (apply_subscriber() upserts by email, maps source status → SSE action, applies suppressions, attaches list memberships). SSE_Migrate_MailPoet and SSE_Migrate_MailerLite are source-specific importers driven by wp sse migrate <source> (dry-run by default, --commit to write). Idempotent — re-running matches by email and produces no duplicates.
Extension points (filters & actions)
The public hooks, all documented in the Developer Reference: Filters & Actions: sse_send() (function); sse_should_send_support_email, sse_support_email_subject, sse_support_email_content, sse_support_email_should_send_to_user, sse_support_email_sent; sse_changelog_product_map, sse_support_assets_dir (both companion-plugin); sse_templates_registry; sse_confirmation_ttl_days; sse_automation_triggers; sse_rate_limit_enabled.
- Developer Reference: Filters & Actions
- Automated Support Emails — Developer Guide
- Tracking & Reporting — the log/event model from a user angle