DEV Community

Cover image for I Built My Own Mailchimp Alternative in 200 Lines of Code
Jbee - codehooks.io
Jbee - codehooks.io

Posted on

I Built My Own Mailchimp Alternative in 200 Lines of Code

Ever looked at your Mailchimp bill and thought "I could build this"?

I did. And I was right.

The Problem

I needed drip emails for my SaaS. Simple 3-step onboarding sequence. Mailchimp wanted $85/month for 5k subscribers. ConvertKit wanted $79/month. ActiveCampaign wanted... let's not talk about it.

For what? Sending time-delayed emails. That's it.

The Solution

Built my own with Codehooks.io (serverless Node.js platform). Total cost: $39/month for infrastructure + email sending. And I own everything.

Here's the entire architecture:

// 1. Config file defines your sequence
{
  "workflowSteps": [
    {
      "step": 1,
      "hoursAfterSignup": 24,
      "template": {
        "subject": "Welcome! πŸŽ‰",
        "heading": "Hi {{name}}!",
        "body": "Thanks for signing up..."
      }
    },
    { "step": 2, "hoursAfterSignup": 96, ... },
    { "step": 3, "hoursAfterSignup": 264, ... }
  ]
}

// 2. Cron job finds subscribers ready for next email
app.job('*/15 * * * *', async (req, res) => {
  const conn = await Datastore.open();

  for (const stepConfig of workflowSteps) {
    const cutoffTime = new Date(now - stepConfig.hoursAfterSignup * 60 * 60 * 1000);

    // Stream subscribers (constant memory usage)
    await conn.getMany('subscribers', {
      subscribed: true,
      createdAt: { $lte: cutoffTime }
    }).forEach(async (subscriber) => {
      if (!subscriber.emailsSent?.includes(stepConfig.step)) {
        // Atomically mark as sent
        const updated = await conn.updateOne(
          'subscribers',
          { _id: subscriber._id, emailsSent: { $nin: [stepConfig.step] } },
          { $push: { emailsSent: stepConfig.step } }
        );

        if (updated) {
          await conn.enqueue('send-email', { subscriberId: subscriber._id, step: stepConfig.step });
        }
      }
    });
  }
});

// 3. Queue worker sends the email
app.worker('send-email', async (req, res) => {
  const { subscriberId, step } = req.body.payload;
  const template = await getTemplate(step);
  await sendEmail(subscriber.email, template.subject, generateHTML(template));
  res.end();
});
Enter fullscreen mode Exit fullscreen mode

That's it. Three pieces:

  1. Config file - Define your steps
  2. Cron job - Find subscribers ready for next email
  3. Queue worker - Send it

Why This Actually Works

Streaming Architecture

Instead of loading all subscribers into memory:

// ❌ Memory issues with 50k+ subscribers
const subscribers = await conn.getMany('subscribers').toArray();

// βœ… Constant memory usage
await conn.getMany('subscribers').forEach(async (sub) => {
  await processSubscriber(sub);
});
Enter fullscreen mode Exit fullscreen mode

Scales to 100k+ subscribers without breaking a sweat.

Race Condition Prevention

The atomic update prevents duplicate emails even if cron jobs overlap:

const updated = await conn.updateOne(
  'subscribers',
  { _id: subscriber._id, emailsSent: { $nin: [step] } }, // Only if not already sent
  { $push: { emailsSent: step } }
);

// Only queue if we won the race
if (updated) {
  await conn.enqueue('send-email', { subscriberId, step });
}
Enter fullscreen mode Exit fullscreen mode

Automatic Retry

If sending fails, worker removes from sent list:

catch (error) {
  await conn.updateOne(
    'subscribers',
    { _id: subscriberId },
    { $pull: { emailsSent: step } }
  );
  // Next cron run will retry
}
Enter fullscreen mode Exit fullscreen mode

Deployment

Literally 3 commands:

npm install -g codehooks
coho create my-drip --template drip-email-workflow
coho deploy
Enter fullscreen mode Exit fullscreen mode

Configure your email provider:

coho set-env EMAIL_PROVIDER "sendgrid"
coho set-env SENDGRID_API_KEY "SG.your-key"
coho set-env FROM_EMAIL "hello@yourdomain.com"
Enter fullscreen mode Exit fullscreen mode

Done. Your drip campaign is running.

The Stack

Codehooks gives you everything in one platform:

  • Serverless functions
  • NoSQL database (MongoDB-compatible)
  • Cron scheduler
  • Queue workers
  • Environment secrets

No AWS Lambda + SQS + CloudWatch + EventBridge nonsense. Just Node.js and deploy.

Cost Breakdown

For 5,000 subscribers:

  • Codehooks Pro: $19/month
  • SendGrid Essentials: $19.95/month
  • Total: $467/year

vs Mailchimp Standard: $1,020/year

For 50,000 subscribers:

  • Codehooks Pro: $19/month
  • SendGrid Pro: $89.95/month
  • Total: $1,307/year

vs Mailchimp: $3,600-4,800/year

The savings scale with your list size.

What You Get

The open source template includes:

βœ… Complete working system

βœ… REST API for subscriber management

βœ… Responsive HTML email templates

βœ… Email audit logging with dry-run mode

βœ… SendGrid, Mailgun, Postmark integrations

βœ… Example configs (onboarding, courses, nurture)

When NOT to Use This

Don't use this if you need:

  • Sub-minute delivery SLAs (use transactional email service)
  • Advanced segmentation UI
  • No-code workflow builder
  • Non-technical team members to manage campaigns

This is for developers who want control and ownership.

Try It

coho create my-campaign --template drip-email-workflow
cd my-campaign
coho deploy
Enter fullscreen mode Exit fullscreen mode

Full code: github.com/codehooks-io/codehooks-io-templates

Docs: codehooks.io/docs


Built this for my own SaaS and figured others might find it useful. No affiliation besides being a happy user.

Questions? Drop them below! πŸ‘‡

You can also read a more comprehensive blogpost here: https://codehooks.io/blog/2026/01/01/webhook-email-automation.

Top comments (2)

Collapse
 
sloan profile image
Sloan the DEV Moderator

We loved your post so we shared it on social.

Keep up the great work!

Collapse
 
restdbjones profile image
Jbee - codehooks.io

Thanks for sharing.