How a message reaches the customer
The notifier picked a move — shipped, out for delivery, delivered, or delayed. Now the sender Lambda has to figure out who to email, at what time of day, whether they even want the email, and with what context attached. Get any of those wrong and the update is worse than no update: a 2am email, a note to a customer who opted out, a “your order shipped” with no tracking link. Four small guardrails sit between the move and the actual send.
Key takeaways
- Contact resolution: the order’s own email beats the account default beats the configured fallback.
- Email is the channel; the sender uses SES outbound with your verified sending domain.
- Quiet hours defer an update to the next morning so nobody gets a middle-of-the-night email.
- Every update ships with the order number, the new status, a tracking link, and an unsubscribe link.
- A delayed move also emails the owner; the customer still gets their warning too.
Four guardrails on every send
Gate 1: resolve the contact
Three places the sender Lambda looks for the customer’s email, in order. First, the order list’s per-order customer_email column — the email the customer gave at checkout, which is right almost every time. Second, an account-default contact in the rules doc (for shops that keep a separate contacts list keyed by customer name). Third, the configured owner fallback — so an update for an order with a missing email goes to a human instead of vanishing. The fallback should rarely fire; if it does, the weekly digest names every order that hit it so the row can be fixed.
Email is the only channel here, on purpose. Shipping updates are exactly the kind of message customers expect in their inbox, and email gives you a clean unsubscribe and a clear sender identity. The sender uses SES outbound with your verified sending domain so the update comes from you, in your voice — not from an address the customer doesn’t recognize.
Gate 2: quiet hours
The check runs every 30 minutes, around the clock, because carriers deliver and scan parcels at all hours. That means a “delivered” status can land at 11pm, and an “out for delivery” scan can happen at 6am. Without a guard, the customer would get a ping in the middle of the night.
Gate 2 reads the rules doc’s quiet-hours setting (default 9pm to 8am, configurable per business). If the current local time is in the quiet window, the sender creates a one-off EventBridge Scheduler rule that fires at the start of sending hours the next morning and exits without sending. The Scheduler invokes the same sender Lambda with the same payload at the deferred time, where Gate 2 will let it through. A delayed warning is the one exception you can configure to bypass quiet hours, since a customer would usually rather know late than not at all.
Gate 3: unsubscribe check
Every update carries an unsubscribe link. If a customer clicks it, a Function URL Lambda writes an unsubscribed flag to the sn-prefs table for that order. Gate 3 reads that flag before every send and, if the customer opted out, skips the send and logs it.
There’s one carve-out. A delayed warning still reaches the owner even if the customer unsubscribed, because the owner needs to know a delivery is running late regardless of the customer’s email preferences. The customer’s own delayed warning still respects their unsubscribe — if they opted out, they won’t get it. The owner’s alert is internal and isn’t a marketing message, so it isn’t subject to the customer’s opt-out.
Gate 4: compose with full context, then ship
The voice doc has one email template per move: a short, friendly message with placeholders for the order number, the new status, the carrier, and the tracking link. The sender Lambda fills the placeholders, appends an unsubscribe link, wraps it in a small branded HTML email, and ships it via SES SendRawEmail from your verified sending domain. The sending identity and DKIM keys live in SES; nothing sensitive sits in the Lambda.
The tracking link is the most useful thing in the email. Rather than make the customer copy a tracking number into a carrier’s site, the link goes straight to the carrier’s tracking page for that parcel, pre-filled. Checking is one click.
A delayed move adds a second recipient: the owner named in the rules doc. The customer still gets their warning (unless they unsubscribed). But the owner now sees it too, with the order number, the expected date, and how many days late it is, so they have what they need to chase the carrier. The delayed template is calmer than you might expect — it tells the customer what’s known, sets a new expectation, and avoids over-promising.
Every send — any move, customer or owner — writes a row to sn-sends in DynamoDB. The next check reads that row and knows not to send the same update again.
Why the guardrails exist
None of these gates are exotic. They’re the kind of small care a thoughtful person would take if they were sending the updates by hand — check you have the right email, don’t email at 2am, don’t email someone who asked you to stop, include enough context that the customer doesn’t have to ask a follow-up question. Putting them in code as four small sequential gates makes them part of the design, not a feature you’re trusting the writer of any one email to remember.
Next post: how a late delivery gets caught — how the notifier spots an order that’s past its date, warns the customer, alerts the owner, and logs the delay so nothing slips silently.
All posts