A few months ago, a friend forwarded me an email with the subject “New sign-in to your account”. She wanted to know if she should click the “This wasn't me” link or just delete it. The email was real. It came from a SaaS product she actually uses. And it looked exactly like every phishing attempt that had ever landed in her inbox: a generic greeting, a vague device label, a single big button, and a footer that didn't name the company until the legal line at the bottom.
She deleted it. A week later, she clicked a phishing link from a spoofed sender for the same product. She had been trained, by years of badly-designed legitimate security emails, to ignore all of them. The product's “security alerts” had become noise, and the attacker exploited the silence.
That is the quiet failure mode of the new-device sign-in email. When the legitimate version looks indistinguishable from a phishing attempt, users learn to delete both. The fix is not louder copy or scarier icons. It is a template that survives a second look.
The biggest signal a user has that a phishing email is fake is what they remember the real one looking like. If your security template is clean, specific, and consistent, suspicious lookalikes stand out. If it is not, every security email is a coin flip.
Why your security email fails the smell test
Most teams write the new-device sign-in email once, ship it, and never look at it again. The default template that ships with auth providers like NextAuth, Clerk, and Supabase is a starting point, not a finished product. Out of the box, the message reads almost identically to a phishing attempt. Here is what users actually compare when they decide whether to trust an email.
| Signal | Phishing Email | Trustworthy Sign-In Alert |
|---|---|---|
| Sender domain | no-reply@security-alerts.example | security@yourbrand.com (matches the product domain) |
| Subject line | Security alert: action required | New sign-in from Chrome on macOS in San Francisco |
| Greeting | Dear user, | Hi Sarah, |
| Device label | Unknown device | Chrome 142 on macOS 15.4 |
| Location | Foreign country, no map | San Francisco, CA (~3 mi from your last sign-in) |
| Time precision | Recently | Today at 9:42 AM PT |
| Primary CTA | Verify account | This wasn't me - secure my account |
| Footer trust signals | Generic legalese | Account email, support address, postal address, IP |
The phishing smell test: what users compare in the first 4 seconds.
The 7 trust signals that earn the open
A user gives a new-device sign-in email a few seconds before they decide whether to trust it. The signals below are what tips the decision, ordered by how much they actually move the needle.
The trust hierarchy
- Specific device + location in the subject line. A phisher does not know the user's city.
- Personalized greeting with first name and the masked email the user signed in with.
- Map snippet or location pin, even if approximate. Visual context beats text.
- Browser + OS + version, not just “a device”.
- Last sign-in comparison(“~3 mi from your last sign-in”) so the user has a frame of reference.
- Two clear paths:“This was me” (no-op) and “This wasn't me” (one-click lockout).
- Verifiable footer: account email, support address, mailing address, and the IP that triggered the sign-in.
Notice what isn't on the list: marketing copy, brand promotions, social links, a footer of legal disclaimers. A security alert is the one transactional email where you should fight to remove things, not add them. Every pixel that is not a trust signal is a trust leak.
Anatomy: the structure that survives scrutiny
Users read security emails the way they read any cold email: top-left first, then top-right, then a diagonal sweep down to the main button. The block order below matches that scan path, so the “is this real?” question gets answered before the user finishes scrolling.
Sender + subject preview pane
Sender: security@yourbrand.com. Not no-reply@, not a third-party domain. Subject: the device, browser, and city. Preview text: the time and the masked account email. For a chunk of users this is the only part of the email they will ever read.
Header: brand + a recognizable mark
Logo top-left at 32px. The brand name as plain text next to it, not as an image alone (image-only logos break in Outlook's dark mode and on slow connections). No marketing tagline, no navigation, no banner image.
One-line summary: who, what, where, when
“Chrome on macOS just signed in to your account from San Francisco, CA at 9:42 AM PT.” This sentence is the entire payload. Everything after it is supporting evidence.
Device card: structured details
A small bordered card with the browser, OS, IP address, and approximate location. Optional: a static map snippet from Mapbox or Google Static Maps. Use a CID-attached image, not a tracker pixel - some clients block remote images by default.
Two CTAs: 'This was me' and 'This wasn't me'
The “This wasn't me” button is the larger one, in the brand's primary color. “This was me” is a secondary text link below. Both go to authenticated, signed URLs that expire in 24 hours, not to a generic login screen.
Verifiable footer
Account email (masked), support email, the company's legal mailing address, and a single line like “You received this because someone signed in to s***h@example.comfrom IP 203.0.113.42.” This footer is why the user trusts the email - it contains facts only the legitimate sender knows.
A production-ready React Email template
Here is the React Email implementation for the structure above. It uses the standard @react-email/components primitives, accepts typed props for the device and location data, and renders cleanly in Gmail, Apple Mail, Outlook (desktop and web), and the default Android client.
import {
Body,
Container,
Head,
Heading,
Hr,
Html,
Link,
Preview,
Section,
Text,
Button,
Img,
} from "@react-email/components";
export interface NewDeviceSigninEmailProps {
firstName: string;
maskedEmail: string;
device: { browser: string; os: string; version: string };
location: { city: string; region: string; approximate: boolean };
signedInAt: Date;
ipAddress: string;
thisWasntMeUrl: string;
thisWasMeUrl: string;
supportEmail: string;
brandName: string;
brandLogoUrl: string;
mailingAddress: string;
}
export default function NewDeviceSigninEmail({
firstName,
maskedEmail,
device,
location,
signedInAt,
ipAddress,
thisWasntMeUrl,
thisWasMeUrl,
supportEmail,
brandName,
brandLogoUrl,
mailingAddress,
}: NewDeviceSigninEmailProps) {
const formattedTime = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeZoneName: "short",
}).format(signedInAt);
const subject = `New sign-in from ${device.browser} on ${device.os} in ${location.city}`;
return (
<Html>
<Head />
<Preview>
{`${formattedTime} - ${maskedEmail}. Not you? Secure your account.`}
</Preview>
<Body style={body}>
<Container style={container}>
<Section style={header}>
<Img
src={brandLogoUrl}
alt={brandName}
width={32}
height={32}
style={logo}
/>
<Text style={brand}>{brandName}</Text>
</Section>
<Heading style={h1}>{subject}</Heading>
<Text style={paragraph}>
Hi {firstName}, someone just signed in to{" "}
<strong>{maskedEmail}</strong>. If this was you, you can ignore
this email. If it wasn't, secure your account now.
</Text>
<Section style={deviceCard}>
<Text style={deviceRow}>
<strong>Device:</strong> {device.browser} {device.version} on{" "}
{device.os}
</Text>
<Text style={deviceRow}>
<strong>Location:</strong>{" "}
{location.approximate ? "~" : ""}
{location.city}, {location.region}
</Text>
<Text style={deviceRow}>
<strong>Time:</strong> {formattedTime}
</Text>
<Text style={deviceRow}>
<strong>IP address:</strong> {ipAddress}
</Text>
</Section>
<Section style={ctaSection}>
<Button href={thisWasntMeUrl} style={primaryButton}>
This wasn't me - secure my account
</Button>
<Text style={secondaryLink}>
<Link href={thisWasMeUrl} style={secondaryAnchor}>
This was me, dismiss this alert
</Link>
</Text>
</Section>
<Hr style={hr} />
<Text style={footer}>
You received this because someone signed in to{" "}
<strong>{maskedEmail}</strong> from IP {ipAddress}.
</Text>
<Text style={footer}>
Questions? Reply to this email or contact{" "}
<Link href={`mailto:${supportEmail}`} style={footerLink}>
{supportEmail}
</Link>
.
</Text>
<Text style={footerSmall}>{mailingAddress}</Text>
</Container>
</Body>
</Html>
);
}
const body = { backgroundColor: "#f6f6f6", fontFamily: "system-ui, sans-serif" };
const container = { backgroundColor: "#ffffff", margin: "40px auto", padding: "32px", maxWidth: "560px", borderRadius: "12px" };
const header = { display: "flex", alignItems: "center", gap: "8px" };
const logo = { borderRadius: "6px" };
const brand = { fontSize: "14px", fontWeight: 600, color: "#0f172a", margin: 0 };
const h1 = { fontSize: "20px", fontWeight: 600, color: "#0f172a", margin: "24px 0 8px" };
const paragraph = { fontSize: "15px", lineHeight: "24px", color: "#334155", margin: "8px 0 24px" };
const deviceCard = { border: "1px solid #e2e8f0", borderRadius: "8px", padding: "16px 20px", marginTop: "8px" };
const deviceRow = { fontSize: "14px", color: "#0f172a", margin: "8px 0", lineHeight: "20px" };
const ctaSection = { textAlign: "center" as const, margin: "28px 0 12px" };
const primaryButton = { backgroundColor: "#dc2626", color: "#ffffff", borderRadius: "6px", padding: "12px 20px", fontSize: "15px", fontWeight: 600, textDecoration: "none" };
const secondaryLink = { fontSize: "13px", color: "#64748b", marginTop: "16px" };
const secondaryAnchor = { color: "#64748b", textDecoration: "underline" };
const hr = { border: "none", borderTop: "1px solid #e2e8f0", margin: "32px 0 20px" };
const footer = { fontSize: "12px", lineHeight: "20px", color: "#64748b", margin: "8px 0" };
const footerLink = { color: "#64748b", textDecoration: "underline" };
const footerSmall = { fontSize: "11px", color: "#94a3b8", marginTop: "16px" };thisWasntMeUrl and thisWasMeUrlshould be signed, single-use tokens that expire in 24 hours. Don't link to a generic login page - that pattern is exactly how phishing kits operate. The link itself is the trust signal.Triggering the email from a Next.js sign-in handler
The template above is just the React component. The other half is the trigger - the bit of server code that detects a new device and fires the email. Below is the pattern with Resend and a server action, using a simple device fingerprint check.
"use server";
import { headers } from "next/headers";
import { Resend } from "resend";
import NewDeviceSigninEmail from "@/emails/new-device-signin";
import { getKnownDevices, recordDevice } from "@/lib/devices";
import { lookupGeo } from "@/lib/geoip";
import { parseUserAgent } from "@/lib/ua";
import { signLockoutToken } from "@/lib/tokens";
const resend = new Resend(process.env.RESEND_API_KEY!);
export async function onSuccessfulSignin(userId: string) {
const h = await headers();
const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "";
const ua = h.get("user-agent") ?? "";
const device = parseUserAgent(ua);
const fingerprint = `${device.browser}:${device.os}:${device.version}`;
const known = await getKnownDevices(userId);
if (known.includes(fingerprint)) return;
const user = await getUser(userId);
const location = await lookupGeo(ip);
await resend.emails.send({
from: "security@yourbrand.com",
to: user.email,
subject: `New sign-in from ${device.browser} on ${device.os} in ${location.city}`,
react: NewDeviceSigninEmail({
firstName: user.firstName,
maskedEmail: maskEmail(user.email),
device,
location,
signedInAt: new Date(),
ipAddress: ip,
thisWasntMeUrl: signLockoutToken(userId, "lockout"),
thisWasMeUrl: signLockoutToken(userId, "dismiss"),
supportEmail: "support@yourbrand.com",
brandName: "Yourbrand",
brandLogoUrl: "https://yourbrand.com/logo.png",
mailingAddress: "123 Main St, San Francisco, CA 94105",
}),
headers: {
"List-Unsubscribe": "<mailto:unsubscribe@yourbrand.com>",
},
});
await recordDevice(userId, fingerprint);
}
function maskEmail(email: string) {
const [local, domain] = email.split("@");
if (local.length <= 2) return `${local[0]}***@${domain}`;
return `${local[0]}***${local.slice(-1)}@${domain}`;
}Copy patterns: the words that trigger or reassure
Subject lines and CTA copy do most of the work in those first few seconds. Every phishing kit reuses the same small set of phrases, so the moment you stop using them, you stop reading like one. Here are the patterns that consistently land vs. the ones that get filtered to spam or deleted on sight.
- New sign-in from Chrome on macOS in San Francisco
- Hi {firstName}, someone signed in to {maskedEmail}
- This wasn't me - secure my account
- Today at 9:42 AM PT (your timezone)
- From IP 203.0.113.42 (~3 mi from your last sign-in)
- Reply to this email or contact support@yourbrand.com
- Security alert: immediate action required
- Dear customer / Dear user / Hello there
- Verify your account / Click here to confirm
- Recently / A few moments ago / Just now
- Unknown device from an unknown location
- Do not reply to this automated message
The pattern is simple: specificity reassures, vagueness alarms. “Verify your account” is the single most-used CTA in phishing kits because it works on users who don't know what action they're actually taking. A legitimate template should always say what the button does: This wasn't me - secure my accounttells the user exactly what will happen on click. Phishing kits can't afford that level of specificity because they don't know what the user expects.
Edge cases that quietly break trust
Once the happy-path template is solid, the edge cases are what separate a competent security email from a great one. The four scenarios below are where most teams ship a generic template and accidentally train users to ignore future alerts.
VPN and corporate network sign-ins
A user on a corporate VPN appears to sign in from a different city every time they switch buildings. Default templates send a fresh alert each time, and after the third one the user creates a Gmail filter to silently archive them. The fix is a soft device-trust score: if the user has signed in from this device fingerprint within the last 30 days, send a downgraded “new location” email, not a full alarm.
Mobile network changes
Mobile users hop between Wi-Fi and cell networks constantly, which changes their public IP and approximate location. Trigger sign-in alerts on device fingerprint, not on IP geolocation alone, or the first hour of every commute will generate an alert.
Failed sign-ins followed by success
If three failed sign-ins precede a successful one from the same IP, the alert should escalate. “3 failed attempts followed by a successful sign-in” is a credential-stuffing signature, and a higher-urgency template (red header, force-password-reset CTA) is warranted. Most providers ship the same template regardless of context.
Accounts with 2FA already enabled
If the user has 2FA on and just passed the challenge, say so in the alert: “A new device passed your 2FA challenge.” That reads as reassuring, not alarming. Send the same generic template you would for a password-only sign-in and you waste the chance to remind the user their setup actually worked.
The phishing smell test: a 60-second QA pass
Before shipping any change to a security email, run it through the checklist below. The goal is to read it the way a suspicious user would, not the way a developer who already knows it's real would.
- Open the email on a phone in dark mode. Is the brand still recognizable? Is the primary CTA still readable?
- Cover the logo. Can you tell which company sent it from copy alone? If not, the email is too generic.
- Read only the subject and preview text. Would a user who doesn't remember signing up know what this email is about?
- Hover over every link. Do they all point to your verified domain? Any redirect through a third party is a phishing signal.
- Check the
Fromaddress against your DMARC policy. A relaxed DMARC policy lets attackers spoof your subdomain. - Confirm the “This wasn't me” link goes to a one-click lockout, not a login form. A login form is what a phisher would build.
- View the email source. Does the HTML contain tracking pixels from third-party domains? Strip them - they trigger spam filters and erode trust.
Gmail bulk sender requirements
Gmail and Yahoo now require DMARC alignment for high-volume senders. New-device sign-in is one of the highest-volume security emails most products send - confirm it passes DMARC, SPF, and DKIM before shipping any template change.
support.google.com
Related security emails to ship together
The new-device sign-in email helps on its own. It works much better as part of a security email family that all share the same layout and copy style. Once users see two or three of these and notice the family resemblance, they learn to recognize a real email from your brand at a glance.
- Password reset email - shares the “single signed link, expires in 1 hour” pattern.
- Email verification email - first contact a user has with your auth flow, sets the visual baseline.
- Magic link email - the “sign in” equivalent of new-device alerts; same trust patterns apply.
- Two-factor enrollment confirmation - one-line acknowledgement that 2FA is now active, with a single link to disable in case the user didn't enroll.
- Account recovery email - higher-urgency template for when the recovery flow itself is triggered.
The new-device sign-in is the security email users see most often, which makes it the one that sets the bar for what “real” looks like coming from your brand. Get it right and every future alert benefits. Get it wrong and even your legitimate alerts get filtered or trashed. Three things to ship this week:
- Replace the default auth-provider template with a typed React Email template that includes device, location, time, and IP.
- Move the “This wasn't me” CTA to a signed, one-click lockout link, not a login screen.
- Audit the
Fromaddress, DMARC alignment, and footer trust signals. The footer is what separates a legitimate alert from a phishing email on second glance.