WebZum Logo
WebZum

From Zero to Website Hero

Sign InSign Up
Back to Blog
awsemailsmsinfrastructurestartup

We Built a Referral System with AWS SES and SNS (And Learned Why Email Is Harder Than SMS)

WebZum Team•September 12, 2025•10 min read
We Built a Referral System with AWS SES and SNS (And Learned Why Email Is Harder Than SMS)

We Built a Referral System with AWS SES and SNS (And Learned Why Email Is Harder Than SMS)

TL;DR: We built a complete referral system using AWS SES (email) and SNS (SMS). Users can invite others via email or text, track referrals, and earn rewards. Cost: $0.10 per 1,000 emails, $0.00645 per SMS. Saved $500/month vs SendGrid/Twilio. Email deliverability is 10x harder than SMS.

The Problem: Growth Requires Referrals

We needed users to invite other businesses. But how?

Traditional approaches:

  • Third-party services: SendGrid ($15/month + $0.50/1K emails), Twilio ($1/month + $0.0079/SMS)
  • DIY SMTP: Deliverability nightmare, IP reputation issues
  • No referrals: Slow organic growth

What we wanted:

  • Low cost (we’re bootstrapped)
  • High deliverability (emails actually reach inboxes)
  • SMS support (some users prefer texting)
  • Tracking (who invited whom, conversion rates)

The insight: AWS SES + SNS gives us everything for 1/10th the cost.

How It Works: The Technical Architecture

1. Referral Link Generation

Every user gets a unique referral link:

async function generateReferralLink(userId: string): Promise<string> {
  // Generate unique referral code
  const referralCode = crypto.randomBytes(8).toString('hex');
  
  // Store in database
  await db.createReferral({
    userId,
    referralCode,
    createdAt: new Date(),
    clicks: 0,
    conversions: 0
  });
  
  // Return shareable link
  return `https://webzum.com?ref=${referralCode}`;
}

Tracking clicks:

// Middleware to track referral clicks
export async function middleware(req: NextRequest) {
  const refCode = req.nextUrl.searchParams.get('ref');
  
  if (refCode) {
    // Store in cookie (lasts 30 days)
    const response = NextResponse.next();
    response.cookies.set('ref_code', refCode, {
      maxAge: 30 * 24 * 60 * 60, // 30 days
      httpOnly: true,
      secure: true
    });
    
    // Track click
    await db.incrementReferralClicks(refCode);
    
    return response;
  }
  
  return NextResponse.next();
}

2. Email Invitations with AWS SES

Setup AWS SES:

import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';

const sesClient = new SESClient({
  region: 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});

async function sendReferralEmail(params: {
  fromEmail: string;
  fromName: string;
  toEmail: string;
  referralLink: string;
}) {
  const command = new SendEmailCommand({
    Source: `${params.fromName} via WebZum <noreply@webzum.com>`,
    Destination: {
      ToAddresses: [params.toEmail]
    },
    Message: {
      Subject: {
        Data: `${params.fromName} invited you to try WebZum`,
        Charset: 'UTF-8'
      },
      Body: {
        Html: {
          Data: generateEmailHTML(params),
          Charset: 'UTF-8'
        },
        Text: {
          Data: generateEmailText(params),
          Charset: 'UTF-8'
        }
      }
    },
    // Track opens and clicks
    ConfigurationSetName: 'webzum-tracking'
  });
  
  try {
    const response = await sesClient.send(command);
    return {
      success: true,
      messageId: response.MessageId
    };
  } catch (error) {
    console.error('SES send failed:', error);
    return {
      success: false,
      error: error.message
    };
  }
}

Email template:

function generateEmailHTML(params: {
  fromName: string;
  toEmail: string;
  referralLink: string;
}): string {
  return `
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>You're Invited to WebZum</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
  <table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f4; padding: 20px;">
    <tr>
      <td align="center">
        <table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden;">
          <!-- Header -->
          <tr>
            <td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px; text-align: center;">
              <h1 style="color: #ffffff; margin: 0; font-size: 28px;">You're Invited! 🎉</h1>
            </td>
          </tr>
          
          <!-- Body -->
          <tr>
            <td style="padding: 40px 30px;">
              <p style="font-size: 16px; line-height: 1.6; color: #333333; margin: 0 0 20px;">
                Hi there!
              </p>
              <p style="font-size: 16px; line-height: 1.6; color: #333333; margin: 0 0 20px;">
                <strong>${params.fromName}</strong> thinks you'd love WebZum—the AI-powered website builder that creates professional websites in minutes.
              </p>
              <p style="font-size: 16px; line-height: 1.6; color: #333333; margin: 0 0 30px;">
                ✨ No coding required<br>
                🚀 Live in 5 minutes<br>
                💰 Free 7-day preview
              </p>
              
              <!-- CTA Button -->
              <table width="100%" cellpadding="0" cellspacing="0">
                <tr>
                  <td align="center">
                    <a href="${params.referralLink}" style="display: inline-block; padding: 16px 32px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: bold; font-size: 16px;">
                      Create Your Website
                    </a>
                  </td>
                </tr>
              </table>
            </td>
          </tr>
          
          <!-- Footer -->
          <tr>
            <td style="background-color: #f8f9fa; padding: 20px 30px; text-align: center;">
              <p style="font-size: 12px; color: #666666; margin: 0;">
                This invitation was sent by ${params.fromName} (${params.toEmail})
              </p>
              <p style="font-size: 12px; color: #666666; margin: 10px 0 0;">
                <a href="https://webzum.com/unsubscribe" style="color: #667eea; text-decoration: none;">Unsubscribe</a>
              </p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>
  `;
}

function generateEmailText(params: {
  fromName: string;
  referralLink: string;
}): string {
  return `
Hi there!

${params.fromName} thinks you'd love WebZum—the AI-powered website builder that creates professional websites in minutes.

✨ No coding required
🚀 Live in 5 minutes
💰 Free 7-day preview

Create your website: ${params.referralLink}

---
This invitation was sent by ${params.fromName}
Unsubscribe: https://webzum.com/unsubscribe
  `.trim();
}

3. SMS Invitations with AWS SNS

Setup AWS SNS:

import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';

const snsClient = new SNSClient({
  region: 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  }
});

async function sendReferralSMS(params: {
  fromName: string;
  toPhone: string;
  referralLink: string;
}) {
  // Shorten URL (SMS has 160 char limit)
  const shortLink = await shortenUrl(params.referralLink);
  
  const message = `${params.fromName} invited you to try WebZum! Create your website in 5 minutes: ${shortLink}`;
  
  const command = new PublishCommand({
    PhoneNumber: params.toPhone,
    Message: message,
    MessageAttributes: {
      'AWS.SNS.SMS.SenderID': {
        DataType: 'String',
        StringValue: 'WebZum'
      },
      'AWS.SNS.SMS.SMSType': {
        DataType: 'String',
        StringValue: 'Transactional' // Higher deliverability
      }
    }
  });
  
  try {
    const response = await snsClient.send(command);
    return {
      success: true,
      messageId: response.MessageId
    };
  } catch (error) {
    console.error('SNS send failed:', error);
    return {
      success: false,
      error: error.message
    };
  }
}

URL shortening (for SMS):

async function shortenUrl(longUrl: string): Promise<string> {
  // Generate short code
  const shortCode = crypto.randomBytes(4).toString('base64url');
  
  // Store mapping
  await db.createShortUrl({
    shortCode,
    longUrl,
    createdAt: new Date()
  });
  
  return `https://webzum.com/s/${shortCode}`;
}

// Redirect handler
export async function GET(req: Request, { params }: { params: { code: string } }) {
  const { code } = params;
  
  const shortUrl = await db.getShortUrl(code);
  
  if (!shortUrl) {
    return NextResponse.redirect('https://webzum.com');
  }
  
  // Track click
  await db.incrementShortUrlClicks(code);
  
  return NextResponse.redirect(shortUrl.longUrl);
}

4. Referral Tracking

Track conversions:

// When user signs up, check for referral code
export async function POST(req: Request) {
  const { email, password } = await req.json();
  
  // Create user account
  const user = await createUser({ email, password });
  
  // Check for referral code in cookie
  const refCode = req.cookies.get('ref_code')?.value;
  
  if (refCode) {
    // Find referrer
    const referral = await db.getReferralByCode(refCode);
    
    if (referral) {
      // Record conversion
      await db.recordReferralConversion({
        referralId: referral.id,
        referrerId: referral.userId,
        referredUserId: user.id,
        convertedAt: new Date()
      });
      
      // Award referrer (e.g., free month, credits)
      await awardReferralBonus(referral.userId);
      
      // Send notification to referrer
      await notifyReferrer(referral.userId, user.email);
    }
  }
  
  return NextResponse.json({ user });
}

Referral dashboard:

async function getReferralStats(userId: string) {
  const referrals = await db.getReferralsByUser(userId);
  
  return {
    totalInvites: referrals.length,
    clicks: referrals.reduce((sum, r) => sum + r.clicks, 0),
    conversions: referrals.reduce((sum, r) => sum + r.conversions, 0),
    conversionRate: (referrals.reduce((sum, r) => sum + r.conversions, 0) / 
                     referrals.reduce((sum, r) => sum + r.clicks, 0)) * 100,
    earnings: referrals.reduce((sum, r) => sum + r.conversions, 0) * 10 // $10 per conversion
  };
}

The Challenges We Solved

Challenge 1: Email Deliverability

Problem: Emails going to spam

Solution: SPF, DKIM, DMARC configuration

# DNS records for webzum.com

# SPF (Sender Policy Framework)
TXT @ "v=spf1 include:amazonses.com ~all"

# DKIM (DomainKeys Identified Mail)
CNAME <selector>._domainkey.<domain> <selector>._domainkey.amazonses.com

# DMARC (Domain-based Message Authentication)
TXT _dmarc "v=DMARC1; p=quarantine; rua=mailto:dmarc@webzum.com"

Warm-up process:

// Gradually increase sending volume
const WARMUP_SCHEDULE = [
  { day: 1, limit: 50 },
  { day: 2, limit: 100 },
  { day: 3, limit: 200 },
  { day: 7, limit: 500 },
  { day: 14, limit: 1000 },
  { day: 30, limit: 5000 }
];

async function sendWithWarmup(email: EmailParams) {
  const daysSinceLaunch = getDaysSince(LAUNCH_DATE);
  const schedule = WARMUP_SCHEDULE.find(s => daysSinceLaunch <= s.day);
  
  const todayCount = await db.getEmailsSentToday();
  
  if (todayCount >= schedule.limit) {
    // Queue for tomorrow
    await db.queueEmail(email);
    return { queued: true };
  }
  
  return await sendEmail(email);
}

Challenge 2: Bounce and Complaint Handling

Problem: AWS SES suspends accounts with high bounce rates

Solution: Automatic bounce handling

// SNS topic for SES notifications
export async function POST(req: Request) {
  const notification = await req.json();
  
  if (notification.Type === 'SubscriptionConfirmation') {
    // Confirm SNS subscription
    await fetch(notification.SubscribeURL);
    return NextResponse.json({ confirmed: true });
  }
  
  const message = JSON.parse(notification.Message);
  
  if (message.notificationType === 'Bounce') {
    // Hard bounce: remove email from list
    if (message.bounce.bounceType === 'Permanent') {
      await db.markEmailAsBounced(message.mail.destination[0]);
    }
  }
  
  if (message.notificationType === 'Complaint') {
    // User marked as spam: unsubscribe immediately
    await db.unsubscribeEmail(message.mail.destination[0]);
  }
  
  return NextResponse.json({ processed: true });
}

Challenge 3: SMS Deliverability (Easier!)

Problem: None! SMS just works.

Why SMS is easier:

  • No spam filters (carrier filtering is minimal)
  • No authentication required (no SPF/DKIM/DMARC)
  • No warm-up period
  • Higher open rates (98% vs 20% for email)

The catch:

  • More expensive ($0.00645/SMS vs $0.0001/email)
  • Character limit (160 chars)
  • Opt-in required (legal requirement)

Challenge 4: Cost Optimization

Problem: SMS costs add up fast

Solution: Smart channel selection

async function sendReferralInvite(params: {
  fromUserId: string;
  toContact: string; // email or phone
  preferredChannel?: 'email' | 'sms';
}) {
  // Detect contact type
  const isPhone = /^\+?[1-9]\d{1,14}$/.test(params.toContact);
  const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(params.toContact);
  
  // Default to email (cheaper)
  let channel = params.preferredChannel || 'email';
  
  // If phone number provided and no preference, check user's SMS budget
  if (isPhone && !params.preferredChannel) {
    const smsRemaining = await getUserSMSCredits(params.fromUserId);
    
    if (smsRemaining > 0) {
      channel = 'sms';
    } else {
      // Fall back to email (if we have it)
      if (!isEmail) {
        return { error: 'No SMS credits remaining' };
      }
      channel = 'email';
    }
  }
  
  if (channel === 'email') {
    return await sendReferralEmail({
      fromUserId: params.fromUserId,
      toEmail: params.toContact,
      referralLink: await generateReferralLink(params.fromUserId)
    });
  } else {
    return await sendReferralSMS({
      fromUserId: params.fromUserId,
      toPhone: params.toContact,
      referralLink: await generateReferralLink(params.fromUserId)
    });
  }
}

The Results: $500/Month Saved

Cost comparison (1,000 invites):

Third-party services:

  • SendGrid: $15/month + $0.50 = $15.50
  • Twilio: $1/month + $7.90 = $8.90
  • Total: $24.40/month

AWS (our solution):

  • SES: $0.10 (email)
  • SNS: $6.45 (SMS)
  • Total: $6.55/month

Savings: $17.85/month per 1,000 invites

At 30,000 invites/month:

  • Third-party: $732/month
  • AWS: $196.50/month
  • Savings: $535.50/month

Additional benefits:

  • Full control over deliverability
  • No vendor lock-in
  • Detailed tracking and analytics
  • Custom email templates

Why This Matters for Startups

Most startups use expensive third-party services for email/SMS. We learned:

Bad: Pay $500/month for SendGrid/Twilio before you have revenue Good: Pay $50/month for AWS SES/SNS, scale as you grow

The startup lesson: Infrastructure costs matter. AWS SES/SNS are 10x cheaper than third-party services, with the same (or better) deliverability.

Key Insights

  1. Email is hard: SPF/DKIM/DMARC, warm-up, bounce handling
  2. SMS is easy: Just works, but expensive
  3. AWS is cheap: 10x cheaper than alternatives
  4. Tracking matters: Know your conversion rates

What’s Next

We’re exploring:

  • WhatsApp integration: AWS SNS supports WhatsApp Business API
  • Email personalization: Dynamic content based on user data
  • A/B testing: Test different email templates
  • Referral rewards: Gamification (leaderboards, bonuses)

But the core insight remains: Own your infrastructure. Don’t rent it.


Try it yourself: Create a WebZum account, go to Settings → Referrals, invite a friend. Watch the email arrive in seconds.

Building a referral system? Key takeaway: AWS SES + SNS = powerful, cheap email/SMS. Don’t pay $500/month for SendGrid when AWS costs $50.

The future of startup infrastructure isn’t third-party services—it’s AWS primitives.

Ready to Build Your Website?

Join hundreds of businesses using WebZum to create professional websites in minutes, not weeks.

Get Started Free
Live in 5 minutesNo credit card required
Home•Free Tools•Blog•Directory•About•Agencies•Partners
FAQ•Privacy•Terms•© 2026 WebZum