Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

Email Campaigns

Setting up and running email campaigns with templates, lead targeting, and delivery tracking

Email Campaigns

Jose Madrid Salsa includes a full email campaign system built on Resend for transactional email delivery. This guide covers setting up campaigns, creating templates, and monitoring delivery.

Architecture Overview

The email system is organized into several modules:

client.ts
sender.ts
campaign-sender.ts
templates.ts
template-library.ts
automation.ts
automation-engine.ts
rate-limit.ts
suppression.ts
queue.ts
logger.ts
types.ts
page.tsx
actions.ts

Prerequisites

Required Environment Variables

RESEND_API_KEY="re_..."           # Resend API key
FROM_EMAIL="hello@josemadrid.net" # Sender email address

If RESEND_API_KEY is not set, all email operations are silently skipped with a console warning.

Creating a Campaign

Create an Email Template

Navigate to /admin/email-templates to create a new template. Templates support variable interpolation using double-brace syntax:

<h1>Hello {{contact_name}},</h1>
<p>We'd love to partner with {{school_name}} for their {{sport}} program.</p>
<p>{{sport_pitch}}</p>

Available template variables:

  • {{school_name}} - Organization name
  • {{contact_name}} - Contact person name
  • {{title}} - Contact's title (e.g., "Athletic Director")
  • {{sport}} - Sport name
  • {{sport_pitch}} - Auto-generated pitch based on sport type
  • {{city}} - City
  • {{state}} - State

Create a Campaign

Navigate to /admin/email-campaigns/new to create a new campaign. Link it to an email template and configure the target audience.

Campaigns are stored in the LeadCampaign table and linked to Lead records that contain contact information.

Send the Campaign

When you trigger the campaign, the runCampaignSender function processes each lead sequentially:

export async function runCampaignSender(campaignId: string) {
  const campaign = await prisma.leadCampaign.findUnique({
    where: { id: campaignId },
    include: { template: true },
  })

  const leads = await prisma.lead.findMany({
    where: {
      campaignId,
      status: 'CONTACT_FOUND',
      email: { not: null },
    },
  })

  for (const lead of leads) {
    // Process template variables
    // Send via Resend
    // Update lead status
    await new Promise(resolve => setTimeout(resolve, 500)) // Rate limit delay
  }
}

Email Sending Mechanism

The core sendEmail function in lib/email.ts uses the Resend SDK:

import { Resend } from 'resend'

export async function sendEmail(options: {
  to: string
  subject: string
  html?: string
  text?: string
}) {
  const resend = new Resend(process.env.RESEND_API_KEY)
  return resend.emails.send({
    from: process.env.FROM_EMAIL || 'no-reply@example.com',
    to: options.to,
    subject: options.subject,
    ...(options.html ? { html: options.html } : { text: options.text || '' }),
  })
}

Campaign Status Flow

CREATED -> SENDING_EMAILS -> COMPLETED

Each lead within a campaign tracks its own status:

Lead StatusDescription
CONTACT_FOUNDReady to send
EMAIL_SENTSuccessfully delivered
EMAIL_FAILEDDelivery failed

Rate Limiting

The campaign sender includes a 500ms delay between emails to respect Resend API rate limits. The lib/email/rate-limit.ts module provides additional rate limiting controls.

Built-in Email Templates

The platform includes several built-in email types:

await sendPasswordResetEmail(email, token)
// Sends branded HTML email with reset link
// Link expires in 1 hour
await sendAdminReplyEmail(to, subject, message)
// Loads template from DB (key: 'admin_reply')
// Falls back to plain text if no template exists
await runCampaignSender(campaignId)
// Processes all leads with CONTACT_FOUND status
// Replaces template variables per-lead
// Tracks sent/failed counts

Monitoring Campaign Progress

Campaign progress is tracked via real-time events:

emitScraperEvent(campaignId, 'info', 'email',
  `[${idx + 1}/${leads.length}] Sending to ${lead.email}`)

The admin panel at /admin/email-campaigns/[id] shows:

  • Total leads in campaign
  • Emails sent vs failed
  • Per-lead delivery status and error messages
  • Campaign completion status

Suppression and Unsubscribe

The platform supports unsubscribe preferences via the UnsubscribePreference model. The suppression module in lib/email/suppression.ts checks these preferences before sending.

Unsubscribe links are processed at /api/unsubscribe.

Email Compliance

Always include an unsubscribe link in marketing emails. The platform tracks unsubscribe preferences per user to comply with CAN-SPAM requirements.

Seeding Email Templates

Use the seed script to populate default templates:

npm run db:seed:email-templates

How is this guide?

Edit on GitHub

Last updated on

On this page