I Built My Own Mailchimp Alternative in 200 Lines of Code

Published: (December 31, 2025 at 08:03 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Architecture Overview

The system consists of three pieces:

  1. Config file – defines the email sequence.
  2. Cron job – finds subscribers ready for the next email.
  3. Queue worker – sends the email.

Config file

{
  "workflowSteps": [
    {
      "step": 1,
      "hoursAfterSignup": 24,
      "template": {
        "subject": "Welcome! 🎉",
        "heading": "Hi {{name}}!",
        "body": "Thanks for signing up..."
      }
    },
    { "step": 2, "hoursAfterSignup": 96, ... },
    { "step": 3, "hoursAfterSignup": 264, ... }
  ]
}

Cron job – find ready subscribers

app.job('*/15 * * * *', async (req, res) => {
  const conn = await Datastore.open();

  for (const stepConfig of workflowSteps) {
    const cutoffTime = new Date(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 });
        }
      }
    });
  }
});

Queue worker – send 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();
});

Memory‑efficient processing

Instead of loading all subscribers into memory:

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

we stream them:

// ✅ Constant memory usage
await conn.getMany('subscribers').forEach(async (sub) => {
  await processSubscriber(sub);
});

This scales to 100 k+ subscribers without breaking a sweat.

Preventing duplicate emails

The atomic update ensures only one cron instance can queue a send for a given subscriber/step:

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

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

If sending fails, the worker rolls back the flag so the next cron run retries:

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

Deployment & configuration

npm install -g codehooks
coho create my-drip --template drip-email-workflow
coho deploy

Set up your email provider (example: SendGrid):

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

Your drip campaign is now running.

What Codehooks gives you

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

All in one platform—no need to stitch together AWS Lambda, SQS, CloudWatch, EventBridge, etc.

Cost comparison

SubscribersMonthly cost (Codehooks + SendGrid)Annual costMailchimp (Standard)
5 000$19 + $19.95 = $38.95$467$1 020
50 000$19 + $89.95 = $108.95$1 307$3 600‑4 800

Savings grow with list size.

Template features

  • Complete working system
  • REST API for subscriber management
  • Responsive HTML email templates
  • Email audit logging with dry‑run mode
  • Integrations: SendGrid, Mailgun, Postmark
  • Example configs (onboarding, courses, nurture)

When not to use this

  • You need sub‑minute delivery SLAs (use a dedicated transactional service)
  • Advanced segmentation UI
  • No‑code workflow builder
  • Non‑technical team members managing campaigns

This solution is aimed at developers who want control and ownership.

Quick start again

coho create my-campaign --template drip-email-workflow
cd my-campaign
coho deploy

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

Questions? Drop them below!

Back to Blog

Related posts

Read more »