HoneyBun · Operations · Verified 2026-07-27

Every message we send, and what fires it

Eighteen automated flows reach an operator, and 628 places in the worker code can page us. Every email goes out through Resend, every text goes out through GHL. This page is the map and the standing procedure, traced from the worker source rather than from the registry, because messaging-registry.js lists 13 flows while 63 functions actually send.

0wired & sending
0gaps
0retired
0send email
0send SMS

The other direction: alerts to us

Everything above goes to an operator. This half goes to Andrew. It is not a list of messages, it is one pipeline that 628 call sites across 118 worker files feed into. What varies is the severity you pass, and severity alone decides which channels fire.

What each severity actually does

SeverityEmailSMSPush Board taskAuto-remediate
Critical168 call sites YesYesYes YesYes
Warning342 sites, plus 59 that omit it YesNoYes YesYes
Info and error13 info, 7 error YesNoYes YesNo

SMS is critical only, by decision. Set 2026-07-24 (board 58d59209) after the Channel 1 routing fix turned the fanout on for the first time and every severity, including info, started texting. A phone that buzzes for information stops meaning anything when it buzzes for an outage.

error is not a routed severity. The dispatcher branches on critical and warning only, so anything else falls to the info branch. That costs it the immediate auto-remediation dispatch, which fires for warning and critical alone. Seven call sites pass it on purpose. Thirteen more get it by accident, see the open items below.

What stops an alert before it sends

Five guards, evaluated in this order. Every one of them exists because of a specific incident, and every suppression still writes a dead-letter record to KV so nothing vanishes silently.

  1. In-isolate burst guard, 60 secondsSynchronous, no await. When one Supabase stall fans out to ten parallel calls in the same isolate, they all read an empty dedup key before any writes it. This collapses the burst before the async check runs.
  2. Kill switch, ALERT_MUTEDead-letter only, no external calls at all. The break-glass for an alert storm.
  3. Loop guardAn alert whose subject or body is about the alert system skips the board fanout. The board is Supabase-backed, so during a Supabase outage that path was the loop vector.
  4. Circuit breaker, 30 sends per 5 minutesTrips for 30 minutes. Critical bypasses it so a warning flood cannot blind an emergency, but critical has its own ceiling at five times the threshold so a genuine critical loop still stops.
  5. Dedupe and rate limitIdentical subject, severity and source inside 15 minutes is suppressed, and the KV layer holds a 4 hour window so an hourly cron cannot double-fire. On top of that, 3 sends per hour per content hash.

How it gets delivered when the primary path is down

A fallback ladder, not a fanout. The first channel that succeeds wins and the rest never run, which is why an SMS can arrive for a warning if email is also broken.

  1. Service binding to hb-provisionThe normal path. Runs the severity routing in the matrix above.
  2. Direct Resend emailFrom [email protected] to [email protected]. Three consecutive binding failures also write a degradation breadcrumb to KV.
  3. Direct GHL SMSRecipient resolved by phone number, never a stored contact id. A hardcoded id rotted twice, on 2026-05-21 and again on 2026-07-24, each time silently. Upsert by phone is self-healing.
  4. Board task POSTSkipped for loop-guard and notify-only alerts.
  5. KV dead-letter, 7 day retentionThe floor. If you are reading dead letters, every channel above failed.

Which workers are loudest

Alert call sites per file, top ten of 118.

api/hb-deploy.js63
api/hb-clients.js35
api/hb-verticals.js31
api/hb-provisioning-worker-v1.js21
api/hb-session.js20
api/gbp/oauth.js18
api/crm/invite.js16
monitoring/hb-territory-reconciliation.js16
api/scheduled/resume_health.js15
api/dcc-drift/index.js15

Where to find them

PieceFileWhat it owns
Primary dispatcherlib/alert-dispatch.jsSeverity routing, dedupe, board task, auto-remediation.
Hardened wrapperlib/send-alert.jsGuard stack and the five-channel fallback ladder.
SMS recipientlib/alert-contact.jsResolves the GHL contact by phone and caches it for 6 hours.
Promotion gatelib/alert-promotion.jsOptional. Holds the board task until an alert recurs. Off by default, fails open.

Standard operating procedure

Read this before touching anything that sends. Most of these rules exist because something already went wrong once.

Adding a new message

  1. Check it is not already sent. Search this page first. A duplicate welcome has shipped once already: day-0 used to send alongside the platform invite and operators got two near-identical emails. The fix was merging them, and the note is still in crm/invite.js.
  2. Pick the transport by channel, not preference. Email is Resend. SMS is GHL via _sendLeadSMS. There is no third option and no reason to add one.
  3. Resolve the SMS recipient by phone at send time. Never store a GHL contact id in config. The stored id drifts and the send silently 404s, which is exactly how the alert SMS channel died twice.
  4. Write the copy for the moment it actually fires. See the retired operator welcome above: it says "your site is live" but fired before go-live, while the site was still noindexed on a staging subdomain.
  5. Make the failure loud. A non-2xx has to reject, not resolve. A send that fails quietly is worse than one that never existed, because the dashboard still says it went.
  6. Add it to messaging-registry.js, and note that the registry's wired_in_v1 field means "the membership API accepts writes", not "this message sends". It is not a health signal.

Rules earned the hard way

Never guard a cross-module sender with typeof fn === 'function' On an undeclared name that is silently always false. It never throws, never logs an error, and the message simply never sends. That is exactly how the operator welcome went dark for months while logging "not available in this context" on every provision.

Never let a send sit behind a swallowed error _sendPurchaseConfirmation threw on its first line and the call site caught it with .catch(). Every paying operator got no confirmation email and no text, and nothing anywhere reported it.

Drip emails carry no SMS, and that is by design The drip system sends email only. Operators get a text at signup because the invite path sends one separately. Any "add a text to a drip" is new plumbing, not a config change.

SMS to Andrew is critical only Set deliberately after an alert flood. Do not restore warning or info texts without a new decision. A noisy source trips the shared alert breaker and blinds the platform.

Transport reference

ChannelProviderEntry pointNotes
Email to operatorsResendapi.resend.com/emails41 send sites. Every email, no exceptions.
SMS to operatorsGHL_sendLeadSMS(env, phone, body)Resolves the contact by phone at send time.
Email to usResend[email protected]Every severity. Lands in [email protected].
SMS to usGHLsendResilientAlertSmsCritical severity only.
PushWeb PushoperatorPush / _alertPushOperator PWA for leads, internal PWA for alerts.

Open items