DEV Community

WyattSterling5738
WyattSterling5738

Posted on

Healthtech Event Notification Payloads: Node.js Email SMS Schema Checks for Invalid Phones

A compliance notice is a record, not a best-effort message. Short answer: validate the event notification against a versioned JSON Schema before rendering email or SMS, then persist an immutable delivery decision and provider receipt; retries belong after that boundary, never before it.

I've been woken by alerts that meant nothing and missed the one that mattered. In healthtech, the dangerous page is the one that looks successful while its payload cannot prove who was notified. The operational question is therefore boring and strict: what page fired, what data did it carry, and can an auditor replay the decision without guessing?

The incident lesson: reject ambiguity at the boundary

The failure pattern is familiar. An event arrives with patient_id, a notice type, and recipients. A renderer quietly turns a missing template variable into an empty string; an SMS adapter accepts a phone value with a display extension; an email path sends a message whose replyTo field was never validated. The transport may return a request ID, yet the resulting record cannot show that the intended notice was rendered.

That ambiguity compounds during a retry storm: one worker sees a timeout, another sees a duplicate event, and neither can tell whether the first message was rendered from the same template revision. The incident review then spends its time reconstructing intent from scattered logs instead of answering the clinical compliance question.

I treat a malformed payload as a 400-class domain decision, even when the transport itself is healthy. Store the original event hash, schema version, validation errors, and a redacted recipient fingerprint. Do not store a full phone number or address in an operational log. That gives the on-call engineer a useful trail without turning the trail into a second protected-data store.

The invariant is simple: no render, enqueue, or retry occurs until the notification contract is valid. A delivery receipt proves an attempt; it does not prove that the attempt represented the right notice.

How should Node.js validate email, SMS, phone, and template variables?

Keep one validation function ahead of both channels. JSON Schema catches shape and required fields; semantic checks catch rules a schema cannot express, such as an international phone number or a variable set that exactly matches the selected template. Validate the rendered subject and body for unresolved tokens too. A template that still contains {{patient_name}} is a failed compliance notice, not a cosmetic defect.

The following Go example shows the decision boundary as a small, transport-neutral interface. The production service can call it from a Node.js worker, but the rule stays independent of a vendor SDK.

type Notice struct {
    EventID       string            `json:"event_id"`
    SchemaVersion string            `json:"schema_version"`
    Channel       string            `json:"channel"`
    To            string            `json:"to"`
    Template      string            `json:"template"`
    Variables     map[string]string `json:"variables"`
}

type Decision struct {
    Accepted bool
    Reasons  []string
}

func validate(n Notice, required []string) Decision {
    d := Decision{Accepted: true}
    if n.EventID == "" || n.SchemaVersion == "" {
        d.Accepted = false
        d.Reasons = append(d.Reasons, "missing event identity or schema version")
    }
    if n.Channel != "email" && n.Channel != "sms" {
        d.Accepted = false
        d.Reasons = append(d.Reasons, "unsupported channel")
    }
    if n.To == "" || n.Template == "" {
        d.Accepted = false
        d.Reasons = append(d.Reasons, "missing recipient or template")
    }
    for _, key := range required {
        if n.Variables[key] == "" {
            d.Accepted = false
            d.Reasons = append(d.Reasons, "missing template variable: "+key)
        }
    }
    return d
}
Enter fullscreen mode Exit fullscreen mode

In Node.js, make the schema validator return structured paths such as /variables/due_date, not a concatenated sentence. Those paths are searchable in logs and test fixtures. Add contract tests for malformed JSON, an invalid phone, an invalid email, and an extra variable that the template does not declare. Your mileage may vary on address syntax: email syntax validation can reject obvious garbage, but domain delivery still depends on sender policy and recipient systems.

What does an auditable delivery record need after JSON schema debugging?

A useful record has two linked rows: a decision row and an attempt row.

Keep it boring. The decision includes event ID, schema version, template revision, channel, validation result, and a hash of the canonical payload. The attempt includes enqueue time, provider-neutral request ID, response class, retry number, and final state. Keep these fields append-only; correcting a mistake should create a compensating record rather than rewriting history.

A small state machine prevents dashboards from lying:

State Meaning Next action
rejected Contract or recipient check failed Alert owner; no send
accepted Payload passed validation Render and enqueue once
attempted Transport request was made Await receipt or timeout
confirmed Receipt matches the attempt Close the notice
expired Confirmation window elapsed Escalate with evidence

At 03:00, I want the alert to say rejected: /to or expired: attempt 2, not “notification failed.” Correlate every log, metric, and trace with the event ID, but avoid putting protected health information in metric labels.

Limits, trade-offs, and the safer fallback

This boundary is a poor fit for low-value broadcast messages where losing one event has no compliance consequence; a simpler queue may be enough there. It is also unsuitable when a policy requires human approval before sending, because schema acceptance is not approval. Stick with a workflow engine or a manual review queue when consent, legal hold, or clinical escalation is the actual gate.

There is a trade-off: strict contracts reject more messages up front and force template owners to coordinate schema revisions. That friction is deliberate. If a team cannot version templates and schemas together, start with a smaller contract and an explicit quarantine queue rather than silently filling missing variables.

References

Top comments (0)