Loading...
Loading...
Scaffold transactional and campaign email infrastructure end-to-end — provider setup, templates, user segmentation, and admin send UI. Use when the user wants to add email to their app — welcome emails, notifications, re-engagement, or bulk campaigns. Triggers on requests like "add email", "set up Resend", "email campaigns", "transactional email", "send emails to users", "welcome email", "notification emails", or any mention of email sending in an app context.
npx skill4agent add tushaarmehtaa/tushar-skills ship-emailclerk_user_idemailidemailnamecreditsplancreated_atlast_active_atI'll wire email for your [framework] app with [database].
Quick decisions:
1. What emails do you need? (transactional, campaigns, or both)
2. Sender: From name and from email? (e.g., "Tushar from Bangers Only <hi@bangersonly.xyz>")
3. Domain verified in Resend? (yes / need to set it up)
Defaults: welcome email + re-engagement campaign, Resend.npm install resend.env.exampleRESEND_API_KEY=re_xxxxx// lib/email.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
interface SendEmailParams {
to: string | string[];
subject: string;
html: string;
from?: string;
replyTo?: string;
}
export async function sendEmail({
to,
subject,
html,
from = 'Your Name <you@yourdomain.com>',
replyTo,
}: SendEmailParams) {
try {
const result = await resend.emails.send({ from, to, subject, html, reply_to: replyTo });
return { success: true, id: result.data?.id };
} catch (error) {
console.error('Email send failed:', error);
return { success: false, error };
}
}{ success, error }[ ] Add MX record to DNS
[ ] Add SPF TXT record: v=spf1 include:resend.com ~all
[ ] Add DKIM TXT records (Resend provides these in dashboard)
[ ] Verify domain status shows "Verified" in Resend dashboard
[ ] Set reply-to to a monitored inbox (not noreply)// lib/emails/welcome.ts
export function welcomeEmail({ name, ctaUrl }: { name: string; ctaUrl: string }): string {
return `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>You're in. Here's what to do first:</p>
<p>
<a href="${ctaUrl}"
style="display: inline-block; background: #000; color: #fff; padding: 12px 24px;
text-decoration: none; border-radius: 6px; font-size: 14px;">
Get started
</a>
</p>
<p>Any questions — just reply to this email.</p>
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
<p style="color: #999; font-size: 11px;">
<a href="{{{unsubscribe_url}}}" style="color: #999;">Unsubscribe</a>
</p>
</div>
`;
}// lib/emails/reengagement.ts
export function reengagementEmail({ name, daysSinceActive }: { name: string; daysSinceActive: number }): string {
return `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>Haven't seen you in ${daysSinceActive} days. [Product] has [one improvement since they last used it].</p>
<p>
<a href="[APP_URL]"
style="display: inline-block; background: #000; color: #fff; padding: 12px 24px;
text-decoration: none; border-radius: 6px; font-size: 14px;">
Pick up where you left off
</a>
</p>
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
<p style="color: #999; font-size: 11px;">
<a href="{{{unsubscribe_url}}}" style="color: #999;">Unsubscribe</a>
</p>
</div>
`;
}sendEmail// After creating the new user:
if (isNewUser) {
await sendEmail({
to: user.email,
subject: 'Welcome',
html: welcomeEmail({ name: user.name || 'there', ctaUrl: process.env.APP_URL! }),
});
}| If table has... | Generate these segments |
|---|---|
| Power (top 20%), Active (middle 60%), Inactive (bottom 20%) |
| New (<7 days), Established (7-30 days), Veteran (30+ days) |
| Free, Paid, Churned |
| Active (<7 days), Dormant (7-30 days), Churned (30+ days) |
-- Dormant users: used some credits but went quiet
SELECT email, name
FROM users
WHERE (initial_credits - credits) > 0
AND last_active_at < NOW() - INTERVAL '7 days'
AND email_unsubscribed = false;POST /api/admin/send-campaign
Body: { segment: string, templateId: string }{ sentCount, failedEmails, errors }// api/admin/send-campaign/route.ts
export async function POST(req: Request) {
const adminSecret = req.headers.get('x-admin-secret');
if (adminSecret !== process.env.ADMIN_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const { segment, templateId } = await req.json();
const users = await getUsersInSegment(segment);
let sentCount = 0;
const failedEmails: string[] = [];
for (const user of users) {
const result = await sendEmail({
to: user.email,
subject: getSubjectForTemplate(templateId),
html: renderTemplate(templateId, user),
});
if (result.success) {
sentCount++;
} else {
failedEmails.push(user.email);
}
}
return Response.json({ sentCount, failedEmails });
}// lib/unsubscribe.ts
import { createHmac } from 'crypto';
export function generateUnsubToken(email: string): string {
return createHmac('sha256', process.env.UNSUB_SECRET!)
.update(email)
.digest('hex')
.slice(0, 16);
}
export function unsubUrl(email: string): string {
const token = generateUnsubToken(email);
return `${process.env.APP_URL}/unsubscribe?email=${encodeURIComponent(email)}&token=${token}`;
}email_unsubscribed boolean DEFAULT falseemail_unsubscribed = falseFlow 1: Transactional
[ ] User signs up → welcome email arrives in inbox (not spam)
[ ] Email shows from verified domain, not @resend.dev
[ ] Reply-to is a real inbox
Flow 2: Campaigns
[ ] Admin send-campaign endpoint requires auth
[ ] Segment query returns correct users
[ ] Failed individual emails don't abort the batch
[ ] Response shows sentCount and failedEmails
Flow 3: Unsubscribe
[ ] Unsubscribe link in every campaign email
[ ] Clicking it marks user as unsubscribed in DB
[ ] Unsubscribed users excluded from future segments
Flow 4: Edge Cases
[ ] sendEmail never throws — returns { success: false } on failure
[ ] Email sending doesn't block the main signup flow
[ ] RESEND_API_KEY in .env.example (not in code)