Use case
SaaS onboarding emails
Welcome, setup, activation, nudge — the four-email sequence every SaaS ships in its first quarter. Idempotent, milestone-aware, opt-out compliant.
An onboarding sequence is half marketing, half transactional. The 'welcome' email is unambiguously transactional (the user just signed up — they expect it). 'Day 7 — still need help?' is unambiguously marketing (they opted in by signing up but you must respect any subsequent opt-out). The patterns below handle both ends cleanly: idempotent scheduling on top of a job queue, milestone gates so you do not send 'finish your setup' to a user who already finished, and one-click unsubscribe on the marketing-side emails.
Anatomy
What every solid implementation has
- 1User-signs-up event fires immediately; queue a welcome email with idempotency key user-welcome-<user_id>.
- 2A scheduler enqueues subsequent emails at relative offsets (day 0, day 3, day 7). Idempotency keys per (user_id, milestone) so a re-trigger does not duplicate.
- 3Each scheduled email checks a milestone gate before sending — "have they completed setup?" — and skips if so.
- 4Marketing-tagged emails include a one-click List-Unsubscribe header (RFC 8058) and respect the user.marketing_opt_in flag.
- 5A central send wrapper short-circuits on hard bounces and unsubscribes; the user record holds the source-of-truth flags.
- 6Webhook events back update which messages landed, opened, clicked — the funnel data product wants.
Implementation
The actual code
Welcome email — fired at signup
Idempotency key derived from the user id means a signup retry never double-welcomes. This one bypasses marketing_opt_in (transactional — the user just signed up).
import { Sndr } from '@rkiza/sndr'; const sndr = new Sndr(process.env.SNDR_API_KEY!); export async function sendWelcome(userId: string) { const user = await db.user.findUniqueOrThrow({ where: { id: userId } }); if (await db.suppression.exists({ address: user.email, kind: 'hard_bounce' })) return; await sndr.emails.send( { from: 'hello@yourdomain.com', to: user.email, subject: `Welcome to Acme, ${user.name.split(' ')[0]}`, html: renderWelcomeHtml(user), text: renderWelcomeText(user), tags: [ { key: 'kind', value: 'transactional' }, { key: 'flow', value: 'onboarding' }, { key: 'step', value: 'welcome' }, ], }, { idempotencyKey: `user-welcome-${userId}` }, ); // Enqueue the rest of the sequence relative to signup. await queue.scheduleAt('onboarding-day-0', { user_id: userId }, addMinutes(new Date(), 60)); await queue.scheduleAt('onboarding-day-3', { user_id: userId }, addDays(new Date(), 3)); await queue.scheduleAt('onboarding-day-7', { user_id: userId }, addDays(new Date(), 7)); }Milestone-gated nudge — Day 3
Check whether the user already hit the milestone before sending. Skipping is silent (no log noise). Honor marketing_opt_in because the nudge is genuinely marketing — the user can opt out and the law / RFCs require we respect that.
export async function sendDay3Nudge(userId: string) { const user = await db.user.findUniqueOrThrow({ where: { id: userId } }); // Respect opt-out from marketing. Transactional sequences bypass // this; "still need help getting set up?" is marketing. if (!user.marketing_opt_in) return; if (await db.suppression.exists({ address: user.email })) return; // Milestone gate — do not nudge users who already completed setup. const setupComplete = await db.workspace.findFirst({ where: { owner_id: userId, setup_completed: true }, }); if (setupComplete) return; await sndr.emails.send( { from: 'team@yourdomain.com', to: user.email, subject: 'Still getting set up?', html: renderDay3NudgeHtml(user), text: renderDay3NudgeText(user), // One-click unsubscribe — Gmail and Yahoo's 2024 sender rules // require this for any bulk-style sender. SNDR's outbound MTA // also adds List-Unsubscribe-Post automatically; do not duplicate. headers: { 'List-Unsubscribe': `<https://yourdomain.com/u/${user.unsubscribe_token}>, <mailto:unsubscribe@yourdomain.com?subject=${userId}>`, }, tags: [ { key: 'kind', value: 'marketing' }, { key: 'flow', value: 'onboarding' }, { key: 'step', value: 'day-3-nudge' }, ], }, { idempotencyKey: `onboarding-day-3-${userId}` }, ); }
Production considerations
What you have to get right
Welcome is transactional; nudges are marketing
Treat them differently in your code: welcome bypasses marketing opt-out, nudges respect it. Tag every send so analytics + suppression tooling can distinguish.
Milestone gates prevent embarrassment
A 'still need help getting set up?' email sent to a power user who set up two hours ago looks broken. Always check whether the user already hit the milestone before sending the corresponding nudge.
Gmail / Yahoo 2024 sender rules
Any bulk-style send (10,000+ per day from your domain) must publish a List-Unsubscribe-Post header for one-click unsub. SNDR adds it automatically; if you set List-Unsubscribe yourself, leave the Post header to SNDR.
Re-trigger safety
Stable idempotency keys per (user_id, step) mean re-enqueuing the same step (e.g. after a worker crash) sends at most one email. Without this you risk double-welcomes during incidents.
New-domain warmup
Onboarding sequences are the highest-volume use of a new sending domain. Ramp gradually — SNDR handles per-MTA warmup automatically, but seeing inbox rate < 95% in the first week is normal and recovers as reputation builds.
Tagging for analytics
Tag every send with (kind=transactional|marketing, flow=onboarding, step=welcome|day-3|...). The SNDR analytics dashboard then filters by tag so you can see per-step open / click / bounce rates without writing custom queries.
FAQ
How many onboarding emails should I send?
Two to four in the first week is a healthy baseline. Welcome (immediate), Day 1 (a quick tip), Day 3 (milestone nudge if relevant), Day 7 (one final help offer). Beyond that, returns diminish fast and unsubscribe rate climbs.
When should I use templates vs raw HTML?
Templates (in code or in the SNDR template store) for anything sent more than once. Raw HTML for one-off operational sends. Template versioning lets a marketing team iterate copy without engineering deploys.
Should I use a marketing platform alongside SNDR?
For high-touch nurture sequences (10+ emails, branching journeys, lead scoring) a dedicated marketing tool earns its keep. For the typical 3-4 email onboarding sequence shown here, SNDR + a job queue is usually simpler and cheaper.
How do I track whether the sequence drove activation?
Tag sends with (flow=onboarding, step=...); SNDR webhooks report delivered/opened/clicked. Join those events against your activation events in your warehouse — the open-to-activation lift per step is the metric to optimise.
Related
Ship this in production today.
Free tier includes 3,000 emails / month. No card required.
Start building
Your next message starts here.
Connect your domain, create an API key, and send with SNDR.