WebZum Logo
WebZum

From Zero to Website Hero

EN
EnglishSvenskaNorskDanskSuomiNederlandsDeutschFrançaisEspañolItalianoPortuguêsPolskiČeštinaMagyarRomânăΕλληνικάУкраїнськаРусскийالعربيةעבריתTürkçe日本語한국어简体中文繁體中文繁體中文(香港)ไทยTiếng ViệtBahasa IndonesiaBahasa Melayuहिन्दीFilipinoPortuguês (Brasil)Español (México)
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 — a free month for the referrer and a free month for the person they invited. 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()
      });
      
      // Bank a free month for BOTH sides — one for the referrer, one for the
      // person who just built. Months go to a wallet, not to anyone's trial
      // clock: a distant deadline converts worse than no deadline at all.
      await awardReferralBonus(referral.userId);
      await awardSignupGift(user.id);
      
      // Tell both of them
      await notifyReferrer(referral.userId, user.email);
      await notifySignupGift(user.id);
    }
  }
  
  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. They get their first month free, you get a free month when they build, and the email arrives 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.

On this page
  1. The Problem: Growth Requires Referrals
  2. How It Works: The Technical Architecture
  3. The Challenges We Solved
  4. The Results: $500/Month Saved
  5. Why This Matters for Startups
  6. What’s Next

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 · CodeMonster LLC