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! πŸ‘‡

Update Log

2025-01-01: Initial release with streaming architecture and multi-provider support

Top comments (0)