Someone signs up for your app. They click around for two minutes, get distracted, and never come back. You never hear from them again, and you have no idea why.
A short set of onboarding emails is one of the cheapest ways to close the gap between "signed up" and "got value". Not a newsletter, not a sales blast: a few plain, helpful emails that show up at the right moment and point to one next step.
This guide walks you through building that set for your first SaaS, even if you built the app with Cursor, Claude Code, Lovable, Bolt, Replit or v0 and have never written a marketing email.
Three quick terms first:
- Onboarding emails: the emails a new user gets in their first 1 to 2 weeks, meant to help them start using the product.
- First win (or activation): the one action that shows a new user your product works for them. For an invoicing app, that might be "sent the first invoice".
- Trigger: the rule that decides when an email goes out, like "24 hours after signup, only if they have not done the first win yet".
One note before we start: this is not about getting your signup or password reset emails out of spam. If those are not arriving, fix your sending domain setup (SPF, DKIM, DMARC) first. Everything below assumes your app can already send email reliably.
Step 1: Pick your one "first win" action
Every onboarding email you write will point at one thing, so pick it carefully.
Ask yourself: what is the smallest action after which a new user can honestly say "oh, this is useful"? It should be:
- Specific: something your database can see, like a row being created.
- Early: possible in the first session, ideally in under 10 minutes.
- Tied to value: not "completed profile", but "created the thing they came for".
Examples (hypothetical products):
- A link-in-bio builder: "published a page with at least one link".
- A meeting notes tool: "uploaded or recorded a first meeting".
- An AI resume helper: "downloaded a first improved resume".
If you have two candidates, pick the one closer to the result the user signed up for.
Step 2: Track three simple things in your database
You do not need a fancy analytics tool for this. You need to know three facts about each user:
- When they signed up.
- Whether (and when) they hit the first win.
- Whether they said no to these emails.
You also need a record of which emails you already sent, so nobody gets the same one twice. Here is a minimal setup for Postgres (Supabase included). Rename the columns to match your app.
-- On your existing profiles/users table
alter table profiles add column if not exists activated_at timestamptz;
alter table profiles add column if not exists last_seen_at timestamptz;
alter table profiles add column if not exists onboarding_emails_off boolean not null default false;
-- One row per onboarding email sent
create table if not exists onboarding_email_log (
user_id uuid not null references profiles(id) on delete cascade,
email_key text not null, -- e.g. 'welcome', 'nudge_24h'
sent_at timestamptz not null default now(),
primary key (user_id, email_key) -- makes double-sends impossible
);
Then set activated_at in the same code path where the first win happens (for example, right after the first invoice is saved), and update last_seen_at when a signed-in user loads the app. If you use Supabase, keep Row Level Security on for the new table and only write to it from your server code.
Step 3: Map a short sequence with clear stop rules
Beginners often copy a 12-email "drip" from a template site. Start smaller. Five emails over 14 days is plenty, and most users will only get two or three of them because the rules skip emails that no longer make sense.
| When it goes out | Who gets it | Its one job | |
|---|---|---|---|
| welcome | Right after signup | Everyone | Point to the first win |
| nudge_24h | About 24 hours after signup | Not activated yet | Remove the most common blocker |
| help_day3 | Day 3 | Still not activated | Ask what got in the way |
| next_step | About 24 hours after activation | Activated users | Show the second most useful thing |
| checkin_day10 | Day 10 | Activated, but not seen for 5+ days | Bring them back with a reason |
Stop rules (write these down, your code will need them):
- The moment a user activates, they stop getting "please get started" emails.
- Nobody gets more than one onboarding email per day.
- If a user turns these emails off, nothing else in this sequence goes to them.
- After day 14, the sequence ends. What happens next belongs to your normal product and billing emails.
This is the key difference from a fixed drip: emails are based on what the user did, not only on the calendar.
Step 4: Write each email like a short note from a person
Plain text, short, one link, and an honest invitation to reply. It is far easier to write than a designed newsletter, and replies are the best feedback you will get this early.
Copy these and fill in the brackets. Keep each one under about 120 words.
EMAIL: welcome
Subject: Your [Product] account is ready - start here
Hi [First name],
Thanks for signing up for [Product]. The fastest way to see if it is
useful for you: [first win, e.g. "create your first invoice"]. It takes
about [X] minutes.
Start here: [deep link straight to that screen]
If anything looks broken or confusing, just reply. I read every reply.
[Your name], founder of [Product]
EMAIL: nudge_24h
Subject: The step most people get stuck on
Hi [First name],
You have not [first win] yet, so here is the part that trips most
people up: [one common blocker, e.g. "connecting your bank account"].
Here is how to do it in [X] minutes: [link to a 60-90 second video or
a short help page]
Or skip it for now and [simpler path], then come back later.
[Your name]
EMAIL: help_day3
Subject: Quick question about [Product]
Hi [First name],
You signed up a few days ago but have not [first win] yet. Totally fine.
Could you hit reply and tell me what got in the way? One line is
plenty: not the right fit, too confusing, no time, something else.
I will use your answer to fix the product.
[Your name]
EMAIL: next_step
Subject: Nice work on your first [thing]
Hi [First name],
You just [first win]. Most people who keep using [Product] do one more
thing next: [second action, e.g. "set up automatic reminders"].
Here is where to do it: [deep link]
[Your name]
EMAIL: checkin_day10
Subject: Your [thing] is waiting
Hi [First name],
Last time you were here you [what they did]. [One concrete reason to
come back, e.g. "Two of your invoices are now overdue."]
Pick up where you left off: [deep link]
[Your name]
You are getting this because you signed up for [Product].
Turn off these tips: [unsubscribe link]
[Your business postal address]
A few rules that make these work:
- Deep links, not your homepage. Send people straight to the screen where the next step happens.
- One link per email. More choices means fewer clicks.
- No fake urgency. No "last chance" in week one.
- Subject says what is inside. Not "Re:" or "Fwd:" tricks. Gmail's sender guidelines explicitly say not to use those unless the message really is a reply or forward.
Step 5: Send them with a small scheduled job
You might be tempted to schedule all five emails at signup. Some providers let you do that (Resend, for example, can schedule an email up to 30 days ahead). The problem: the user's behavior changes after you schedule, so you would have to cancel emails every time someone activates.
A simpler pattern for beginners: run one job on a schedule that asks "who should get which email right now?" and sends only those. Two common ways to run it:
- Vercel Cron Jobs: a route in your Next.js app that Vercel calls on a schedule. On the free Hobby plan, cron jobs can only run once per day, and the exact time can drift within the hour. That is fine for days 3 and 10, less precise for "24 hours".
-
Supabase Cron: Supabase supports
pg_cronpluspg_net, so Postgres can call an Edge Function on any schedule you set, such as every hour.
Send the welcome email directly from your signup code, since it should arrive right away. The job handles everything else.
Here is the core query for one email, nudge_24h, in Postgres:
select p.id, p.email, p.first_name
from profiles p
where p.created_at < now() - interval '24 hours'
and p.created_at > now() - interval '3 days'
and p.activated_at is null
and p.onboarding_emails_off = false
and not exists (
select 1 from onboarding_email_log l
where l.user_id = p.id and l.email_key = 'nudge_24h'
);
And the send step, using the Resend Node SDK as an example (any provider works the same way). It writes to the log first, so if the job runs twice, the unique key stops a second send:
// Inside your scheduled job, for each user from the query above
const { error: logError } = await db
.from("onboarding_email_log")
.insert({ user_id: user.id, email_key: "nudge_24h" });
if (!logError) {
await resend.emails.send({
from: "Sam at Example <sam@updates.example.com>",
to: user.email,
replyTo: "sam@example.com",
subject: "The step most people get stuck on",
text: nudgeText(user), // your template from Step 4
});
}
Before turning it on for real users, add a "dry run" mode that only logs who would get which email, and run it against your own test accounts.
Step 6: Stay legal and out of the spam folder
This part is short but important. (Not legal advice; check the rules where you and your users live.)
Know which emails are "commercial". Under the US CAN-SPAM Act, the FTC separates commercial messages (promoting your product) from transactional or relationship messages (like account and transaction details). Onboarding tips that encourage people to use or buy your product may count as commercial, and the FTC says to read the transactional categories narrowly. The simple, safe choice: treat every email in this sequence, except a pure account notice, as commercial.
For commercial email, CAN-SPAM requires:
- Accurate From and subject lines.
- Your valid physical postal address.
- A clear way to opt out, honored within 10 business days.
- Your opt-out has to keep working for at least 30 days after you send.
Follow Gmail's sender guidelines:
- Only email people who signed up with you.
- Make unsubscribing easy. If you ever send 5,000+ messages a day to Gmail, marketing messages must support one-click unsubscribe and show a visible unsubscribe link.
- Use the same From address for each category (for example, account emails from one address, tips from another).
- Keep your spam rate in Google Postmaster Tools below 0.10%, and never let it reach 0.30%.
In practice: add an unsubscribe link that sets onboarding_emails_off = true, include your postal address in the footer, and keep password resets and receipts in a separate stream so turning off tips never blocks a login email.
Step 7: Measure the right numbers every week
Open rates are tempting, but they are unreliable. Google even notes in its sender guidelines that it does not track open rates and cannot verify third-party open numbers. Track what users do instead.
Once a week, write down:
- Activation rate: of the people who signed up last week, how many hit the first win within 7 days?
- Time to first win: for those who activated, how long did it take?
-
Per email: how many people activated within 48 hours of getting
nudge_24horhelp_day3? - Replies: read every one, and tag them (confusing, bug, wrong fit, no time).
Then change one thing at a time. If help_day3 replies keep saying "I could not find the import button", the fix is probably in the product, not the email.
Copy-paste prompt for your AI coding tool
Paste this into Cursor, Claude Code, Lovable, Bolt or Replit. Adjust the bracketed parts.
I want behavior-based onboarding emails for my SaaS. Before writing code,
read my project and show me a plan. Do not change anything until I approve.
My first win action is: [describe it, e.g. "user creates first invoice"].
1. Add activated_at, last_seen_at and onboarding_emails_off to my users or
profiles table, and an onboarding_email_log table with a unique
(user_id, email_key) key. Keep Row Level Security on; only server code
writes to the log.
2. Set activated_at where the first win happens, only if it is still null.
3. Send a "welcome" email from the signup flow.
4. Create one scheduled job that sends nudge_24h, help_day3, next_step
and checkin_day10 using these rules: [paste the table from Step 3].
Insert into the log before sending. Max one onboarding email per user
per day. Never email users with onboarding_emails_off = true.
5. Add an unsubscribe link and page that sets onboarding_emails_off = true,
and put my postal address in the email footer.
6. Add a DRY_RUN env var that logs who would get which email without
sending. Keep all API keys server-side.
Common mistakes to avoid
- Sending a feature tour. Five features in one email means zero clicks. One email, one step.
- Nagging people who already activated. This is what stop rules are for.
- Using a no-reply address. Replies are your best early feedback. Use a real inbox.
- Mixing tips with password resets. Keep account emails separate so an unsubscribe never blocks a login.
- Judging emails by opens. Judge them by activation and replies.
Action summary
Do this in the next two evenings:
- Write down your one first win action.
- Add the three tracking columns and the email log table.
- Copy the sequence table and stop rules from Step 3.
- Fill in the five templates with your product details and deep links.
- Send the welcome email from signup and set up one scheduled job for the rest, starting in dry run mode.
- Add an unsubscribe link, your postal address, and a real reply-to inbox.
- Every week, check activation rate and read every reply.
Your onboarding emails do not need to be clever. They need to show up when someone is stuck, point to one next step, and stop when the job is done.
Top comments (1)
Good point that onboarding mail should point to one next step. I'd send the first email only if the user hasn't reached the first win within a few hours, and skip it for anyone who already has. Mailing activated users a 'get started' nudge is what pushes people to mark it as spam.