Own your email automation. Deploy in 5 minutes.
A production-ready drip email system that scales from 10 to thousands of subscribers. Configure unlimited workflow steps in a single JSON file. No vendor lock-in, complete flexibility.
You own the codeβadd steps, modify templates, integrate with any system, and switch email providers anytime. The streaming architecture handles high numbers of subscribers with constant memory usage, while one config file and one cron job manages everything. Codehooks provides serverless functions, database, scheduling, and queues in one platform with zero DevOps:
coho create β coho deploy β Done
No servers to manage, no infrastructure to configure. Just edit stepsconfig.json and redeploy.
Configure unlimited steps via stepsconfig.json. Add 3 steps or 30 - same architecture, same simplicity.
- β
Dynamic Step Configuration - Add unlimited steps via
stepsconfig.json - β Integrated Email Templates - Templates defined alongside workflow steps
- β Single Cron Job - One intelligent batch processor for all steps
- β Time-Based Scheduling - Each step runs X hours after signup
- β
Queue-Based Delivery - Reliable background processing with
conn.enqueue() - β Multiple Email Providers - SendGrid, Mailgun, and Postmark REST API integration
- β Intelligent Rate Limiting - Prevents hitting provider API limits with automatic retry
- β Prevents Duplicates - Each subscriber receives each email only once
- β Subscriber Management - Full CRUD API
- β Professional Design - Beautiful, responsive emails
- β Streaming Architecture - Memory-efficient processing for large volumes of subscribers
- β Email Audit Log - Complete tracking of all sends, including dry-run mode
Here's what your subscribers will receive:
The template features a modern, responsive design with a purple gradient header, personalized content, and clear call-to-action buttons.
Simple architecture with config file-based configuration:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β stepsconfig.json β
β { β
β "workflowSteps": [ β
β { β
β "step": 1, β
β "hoursAfterSignup": 24, β
β "template": { subject, heading, body, ... } β
β }, β
β { ... more steps ... } β
β ] β
β } β
βββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β Loaded on startup
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SINGLE CRON JOB (runs every 15 min) β
β β
β For each step in config: β
β 1. Calculate cutoff: now - hoursAfterSignup β
β 2. Find subscribers who: β
β - Haven't received this step β
β - Signed up before cutoff β
β 3. Queue them for sending β
βββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β conn.enqueue('send-email', {...})
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β QUEUE WORKER β
β β’ Sends email via API β
β β’ Updates subscriber.emailsSent array β
β β’ Marks step complete β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The default stepsconfig.json defines a 3-step workflow:
{
"workflowSteps": [
{
"step": 1,
"hoursAfterSignup": 24,
"template": {
"subject": "Welcome to Our Community! π",
"heading": "Welcome, {{name}}!",
"body": "Thank you for joining...",
"buttonText": "Get Started",
"buttonUrl": "https://example.com/get-started"
}
},
{
"step": 2,
"hoursAfterSignup": 96,
"template": { ... }
},
{
"step": 3,
"hoursAfterSignup": 264,
"template": { ... }
}
]
}Add more steps by editing stepsconfig.json and redeploying. The single cron job automatically processes all configured steps!
coho create my-drip-campaign --template drip-email-workflow
cd my-drip-campaign
npm install
coho deploySendGrid:
coho set-env EMAIL_PROVIDER "sendgrid"
coho set-env SENDGRID_API_KEY "SG.your-api-key"
coho set-env FROM_EMAIL "[email protected]"
coho set-env FROM_NAME "Your Company"Mailgun:
coho set-env EMAIL_PROVIDER "mailgun"
coho set-env MAILGUN_API_KEY "your-api-key"
coho set-env MAILGUN_DOMAIN "mg.yourdomain.com" # Your Mailgun sending domain
coho set-env MAILGUN_EU "true" # Set to "true" if using EU Mailgun account, "false" or omit for US
coho set-env FROM_EMAIL "[email protected]" # Must be from a verified/authorized domain
coho set-env FROM_NAME "Your Company"Important Notes:
MAILGUN_DOMAIN: The sending domain configured in your Mailgun account (e.g.,mg.yourdomain.com)FROM_EMAIL: Must be from a verified/authorized domain, but doesn't need to matchMAILGUN_DOMAINexactly (e.g., can be[email protected]whileMAILGUN_DOMAINismg.yourdomain.com)MAILGUN_EU: Set to"true"if you're using an EU Mailgun account, otherwise omit or set to"false"
Postmark:
coho set-env EMAIL_PROVIDER "postmark"
coho set-env POSTMARK_API_KEY "your-server-token"
coho set-env FROM_EMAIL "[email protected]" # Must have a verified sender signature
coho set-env FROM_NAME "Your Company"Important Notes:
- Get your Server Token from: https://account.postmarkapp.com/servers
- Verify your sender signature (single email) or domain before sending
- Postmark automatically generates a plain text version from your HTML
Default is 3 steps. To customize, edit stepsconfig.json:
{
"workflowSteps": [
{
"step": 1,
"hoursAfterSignup": 24,
"template": {
"subject": "Your custom subject",
"heading": "Hello {{name}}!",
"body": "Your email content here...",
"buttonText": "Click Here",
"buttonUrl": "https://example.com",
"logoUrl": "https://example.com/logo.png"
}
},
{
"step": 2,
"hoursAfterSignup": 72,
"template": { ... }
}
]
}Then redeploy:
coho deploycurl -X POST https://your-project.api.codehooks.io/dev/subscribers \
-H "Content-Type: application/json" \
-H "x-apikey: YOUR_API_KEY_HERE" \
-d '{
"name": "John Doe",
"email": "[email protected]"
}'That's it! The cron job runs every 15 minutes and processes all steps automatically.
Get your API key:
coho add-token --description "Drip campaign"Use this API key in the x-apikey header for all API requests.
This template includes ready-to-use example configurations. See CONFIG-EXAMPLES.md for detailed documentation.
Standard 3-Step (Default) - stepsconfig.json
# Already active - 1 day, 4 days, 11 days5-Step Nurture Campaign - stepsconfig.5-step.example.json
cp stepsconfig.5-step.example.json stepsconfig.json
# Day 1, Day 3, Week 1, Week 2, Month 1Daily Course Drip - stepsconfig.course-daily.example.json
cp stepsconfig.course-daily.example.json stepsconfig.json
# 7 daily emails for educational contentAggressive Onboarding - stepsconfig.aggressive-onboarding.example.json
cp stepsconfig.aggressive-onboarding.example.json stepsconfig.json
# 1hr, 4hrs, 12hrs, 1d, 2d, 4d, 1wFast Testing - stepsconfig.testing.example.json
cp stepsconfig.testing.example.json stepsconfig.json
# 5min, 10min, 15min intervals for testingAfter copying, deploy:
coho deployFor full configuration options and custom examples, see CONFIG-EXAMPLES.md.
The stepsconfig.json file is loaded at startup using ES module imports:
import workflowConfig from './stepsconfig.json' assert { type: 'json' };
// Runs every 15 minutes
app.job('*/15 * * * *', async (req, res) => {
const workflowSteps = workflowConfig.workflowSteps;
// Check each step and stream subscribers ready for that step
for (const stepConfig of workflowSteps) {
const { step, hoursAfterSignup } = stepConfig;
const cutoffTime = new Date(now - hoursAfterSignup * 60 * 60 * 1000);
// Stream subscribers ready for this step (memory efficient)
const cursor = conn.getMany('subscribers', {
subscribed: true,
createdAt: { $lte: cutoffTime }
});
await cursor.forEach(async (subscriber) => {
// Check if subscriber hasn't received this step yet
if (!subscriber.emailsSent || !subscriber.emailsSent.includes(step)) {
try {
// Atomically mark as sent and queue
const result = await conn.updateOne(
'subscribers',
{ _id: subscriber._id, emailsSent: { $nin: [step] } },
{ $push: { emailsSent: step } }
);
// Only queue if update succeeded (returns updated document)
if (result) {
await conn.enqueue('send-email', {
subscriberId: subscriber._id,
step: step
});
}
} catch (error) {
console.error('Failed to update subscriber:', error);
}
}
});
}
});Key Insights:
-
Streaming architecture: Uses
cursor.forEach()instead oftoArray()for memory efficiency:- Processes subscribers one at a time rather than loading all into memory
- Scales to high numbers of subscribers without memory issues
- Based on Codehooks streaming data pattern
-
Time-based scheduling: Each step is checked independently based on
createdAttimestamp:- Step 1 sends to everyone who signed up 24+ hours ago (and hasn't received step 1)
- Step 2 sends to everyone who signed up 96+ hours ago (and hasn't received step 2)
- Step 5 sends to everyone who signed up 720+ hours ago (and hasn't received step 5)
-
Race condition prevention: The cron job atomically marks steps as sent BEFORE queueing:
- Uses
$nin(not in) query to ensure step isn't already marked - Only queues if the database update returns an updated document
- Returns null if another process already added the step (race condition)
- Prevents duplicate queue entries even if cron runs overlap
- Uses
-
Automatic retry on failure: If email sending fails:
- Worker removes the step from
emailsSentarray - Next cron run will detect and re-queue the subscriber
- Ensures no emails are lost due to temporary failures
- Worker removes the step from
-
Intelligent rate limiting: Prevents overwhelming email provider APIs:
- Tracks sends per hour in database
- Cron job respects configurable rate limits (default: 100 emails/hour)
- Workers detect 429 errors and retry with exponential backoff (5min β 15min β 30min)
- Emails delayed, never lost
The template includes intelligent rate limiting to prevent hitting email provider API limits:
How it works:
- Cron job: Only queues up to 25 emails per 15-minute run (100/hour max)
- Database tracking: Records actual sends per hour to prevent over-queueing
- Worker retry: Detects 429 (rate limit) errors and retries with delays
Configuration:
# Set rate limits based on your provider plan
coho set-env SENDGRID_RATE_LIMIT "100" # emails per hour
coho set-env MAILGUN_RATE_LIMIT "100"
coho set-env POSTMARK_RATE_LIMIT "100"
coho set-env MAX_EMAILS_PER_CRON_RUN "25" # per 15-minute runMonitor rate limit status:
curl https://your-project.api.codehooks.io/dev/rate-limit-status \
-H "x-apikey: YOUR_API_KEY_HERE"Response:
{
"provider": "sendgrid",
"rateLimit": 100,
"sentThisHour": 47,
"remaining": 53,
"percentUsed": 47,
"status": "ok"
}See RATE_LIMITING.md for complete details.
curl -X POST https://your-project.api.codehooks.io/dev/subscribers \
-H "Content-Type: application/json" \
-H "x-apikey: YOUR_API_KEY_HERE" \
-d '{
"name": "John Doe",
"email": "[email protected]"
}'curl https://your-project.api.codehooks.io/dev/subscribers?subscribed=true \
-H "x-apikey: YOUR_API_KEY_HERE"Response shows which steps each subscriber has received:
{
"subscribers": [
{
"id": "abc123",
"email": "[email protected]",
"emailsSent": [1, 2, 3], // Received steps 1, 2, 3
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
}curl https://your-project.api.codehooks.io/dev/subscribers/:id \
-H "x-apikey: YOUR_API_KEY_HERE"curl -X POST https://your-project.api.codehooks.io/dev/subscribers/:id/unsubscribe \
-H "x-apikey: YOUR_API_KEY_HERE"curl https://your-project.api.codehooks.io/dev/templates \
-H "x-apikey: YOUR_API_KEY_HERE"Returns default templates for all configured steps.
curl -X POST https://your-project.api.codehooks.io/dev/templates \
-H "Content-Type: application/json" \
-H "x-apikey: YOUR_API_KEY_HERE" \
-d '{
"step": 1,
"subject": "Welcome! π",
"heading": "Hi {{name}}, welcome!",
"body": "We are excited to have you...",
"buttonText": "Get Started",
"buttonUrl": "https://example.com",
"logoUrl": "https://example.com/logo.png"
}'Placeholders: {{name}}, {{email}}
# Get all email logs (latest 100)
curl https://your-project.api.codehooks.io/dev/email-log \
-H "x-apikey: YOUR_API_KEY_HERE"
# Filter by subscriber
curl https://your-project.api.codehooks.io/dev/email-log?subscriberId=abc123 \
-H "x-apikey: YOUR_API_KEY_HERE"
# Filter by step
curl https://your-project.api.codehooks.io/dev/email-log?step=1 \
-H "x-apikey: YOUR_API_KEY_HERE"
# Filter by success status
curl https://your-project.api.codehooks.io/dev/email-log?success=false \
-H "x-apikey: YOUR_API_KEY_HERE"
# Filter dry-run emails
curl https://your-project.api.codehooks.io/dev/email-log?dryRun=true \
-H "x-apikey: YOUR_API_KEY_HERE"
# Get more results (up to 1000)
curl https://your-project.api.codehooks.io/dev/email-log?limit=500 \
-H "x-apikey: YOUR_API_KEY_HERE"curl https://your-project.api.codehooks.io/dev/email-log/stats \
-H "x-apikey: YOUR_API_KEY_HERE"Returns statistics about sent emails:
{
"total": 150,
"successful": 145,
"failed": 5,
"dryRun": 20,
"byStep": {
"1": 50,
"2": 48,
"3": 47
},
"byProvider": {
"sendgrid": 130,
"mailgun": 20
},
"recentErrors": [
{
"email": "[email protected]",
"step": 2,
"error": "API key invalid",
"sentAt": "2025-01-15T10:00:00.000Z"
}
]
}curl https://your-project.api.codehooks.io/dev/rate-limit-status \
-H "x-apikey: YOUR_API_KEY_HERE"Returns current rate limit usage:
{
"provider": "sendgrid",
"rateLimit": 100,
"currentHour": "2025-01-15T14:00:00.000Z",
"sentThisHour": 47,
"remaining": 53,
"percentUsed": 47,
"status": "ok"
}curl https://your-project.api.codehooks.io/dev/Shows current workflow configuration:
{
"status": "ok",
"version": "4.1.0",
"configuration": {
"workflowSteps": [
{ "step": 1, "hoursAfterSignup": 24 },
{ "step": 2, "hoursAfterSignup": 96 },
{ "step": 3, "hoursAfterSignup": 264 }
]
},
"endpoints": {
"health": "/",
"subscribers": "/subscribers",
"templates": "/templates",
"emailLog": "/email-log",
"emailLogStats": "/email-log/stats",
"rateLimitStatus": "/rate-limit-status"
},
"rateLimiting": {
"enabled": true,
"rateLimit": 100,
"maxPerCronRun": 25
}
}{
_id: "abc123",
name: "John Doe",
email: "[email protected]",
subscribed: true,
createdAt: "2025-01-15T10:00:00.000Z", // Used to calculate readiness
updatedAt: "2025-01-15T10:00:00.000Z",
emailsSent: [1, 2, 3] // Which steps completed
}{
_id: "def456",
step: 1,
subject: "Welcome! π",
heading: "Welcome, {{name}}!",
body: "Thank you for joining...",
buttonText: "Get Started",
buttonUrl: "https://example.com",
logoUrl: "https://example.com/logo.png",
createdAt: "2025-01-15T10:00:00.000Z",
updatedAt: "2025-01-15T10:00:00.000Z"
}{
_id: "ghi789",
subscriberId: "abc123",
email: "[email protected]",
name: "John Doe",
step: 1,
subject: "Welcome! π",
sentAt: "2025-01-15T10:00:00.000Z",
dryRun: false, // true if sent in dry-run mode
success: true, // false if send failed
provider: "sendgrid", // or "mailgun"
error: "..." // only present if success is false
}The email log provides a complete audit trail of all email sends, including:
- Successful sends (real and dry-run)
- Failed attempts with error messages
- Which provider was used
- Timestamp of each send attempt
Templates are defined in stepsconfig.json with full customization options:
{
"step": 1,
"hoursAfterSignup": 24,
"template": {
"subject": "Email subject line",
"heading": "Main heading with {{name}} placeholder",
"body": "Email body text (supports \\n for line breaks)",
"buttonText": "Call to Action",
"buttonUrl": "https://example.com/action",
"logoUrl": "https://example.com/logo.png"
}
}Template Placeholders:
{{name}}- Subscriber's name{{email}}- Subscriber's email
Default Templates:
The default stepsconfig.json includes 3 templates:
- Step 1: "Welcome to Our Community! π"
- Step 2: "Quick Tips to Get You Started π‘"
- Step 3: "We'd Love to Hear From You! π¬"
Overriding Templates:
You can override config templates at runtime via the /templates API for advanced use cases.
curl https://your-project.api.codehooks.io/dev/curl https://your-project.api.codehooks.io/dev/subscribers \
-H "x-apikey: YOUR_API_KEY_HERE"Look at emailsSent array to see which steps completed.
# Check recent email sends
curl https://your-project.api.codehooks.io/dev/email-log \
-H "x-apikey: YOUR_API_KEY_HERE"
# Check for failed sends
curl https://your-project.api.codehooks.io/dev/email-log?success=false \
-H "x-apikey: YOUR_API_KEY_HERE"
# View statistics
curl https://your-project.api.codehooks.io/dev/email-log/stats \
-H "x-apikey: YOUR_API_KEY_HERE"The email log tracks every send attempt (success and failure) and includes dry-run sends for testing.
coho logs --followCommon log messages:
π [Cron] Starting drip email batch processing...β [Cron] Step 1: Checked 10 subscribers, queued 3 emails (24h after signup)π [Cron] Step 2: Checked 5 subscribers, already sent to allπ [Cron] Total: 15 subscriber-step combinations checkedβ [Cron] Batch complete: Queued 3 total emailsπ¨ [Worker] Processing email for [email protected], step 1β [Worker] Step 1 email sent to [email protected]
Test the entire workflow without sending actual emails:
# Enable dry run mode
coho set-env DRY_RUN "true"
# Add test subscribers and watch logs
coho logs --followIn dry run mode:
- All workflow logic executes normally
- Subscribers are marked as having received emails
- Queue workers process jobs
- No actual emails are sent - only logged
Look for these log messages:
β οΈ DRY RUN MODE ENABLED - Emails will be logged but not sent
π§ [DRY RUN] Would send email:
To: [email protected]
Subject: Welcome to Our Community! π
HTML length: 2847 characters
Provider: sendgrid
From: Your Company <[email protected]>
Disable dry run mode:
coho set-env DRY_RUN "false"
# or remove it entirely
coho remove-env DRY_RUNEdit stepsconfig.json for fast testing:
{
"workflowSteps": [
{ "step": 1, "hoursAfterSignup": 0.083, "template": { ... } }, // 5min
{ "step": 2, "hoursAfterSignup": 0.166, "template": { ... } }, // 10min
{ "step": 3, "hoursAfterSignup": 0.25, "template": { ... } } // 15min
]
}Deploy and add a test subscriber:
coho deploy
curl -X POST https://your-project.api.codehooks.io/dev/subscribers \
-H "Content-Type: application/json" \
-H "x-apikey: YOUR_API_KEY_HERE" \
-d '{"name":"Test User","email":"[email protected]"}'Watch logs:
coho logs --followYou should see emails queued within 15 minutes (next cron run).
Reset stepsconfig.json to production values after testing!
- Sign up at https://sendgrid.com (free: 100 emails/day)
- Verify sender email
- Create API key: Settings β API Keys
- Configure:
coho set-env SENDGRID_API_KEY "SG.your-key"
- Sign up at https://mailgun.com (free: 5,000 emails/month)
- Verify your domain (e.g.,
yourdomain.com) and set up sending domain (e.g.,mg.yourdomain.com) - Authorize sender addresses in Settings β Sending β Authorized Recipients (or verify domain)
- Get API key: Settings β API Security
- Configure:
coho set-env MAILGUN_API_KEY "your-key" coho set-env MAILGUN_DOMAIN "mg.yourdomain.com" # Your Mailgun sending domain coho set-env FROM_EMAIL "[email protected]" # Must be authorized in Mailgun coho set-env MAILGUN_EU "true" # Only if using EU account
Note: FROM_EMAIL must be from an authorized sender address or verified domain. It doesn't need to match MAILGUN_DOMAIN exactly.
- Sign up at https://postmarkapp.com (free: 100 emails/month)
- Create a Server (or use the default one)
- Add a Sender Signature (single email) or verify your domain
- Get Server Token: Select your server β API Tokens
- Configure:
coho set-env POSTMARK_API_KEY "your-server-token" coho set-env FROM_EMAIL "[email protected]" # Must have verified sender signature
Note: Postmark requires sender signature verification for single email addresses, or domain verification for all emails from a domain. It automatically generates a plain text version from your HTML email.
Default is every 15 minutes. To run more/less frequently, edit index.js:
// Every 5 minutes (more responsive)
app.job('*/5 * * * *', async (req, res) => { ... });
// Every hour (less load)
app.job('0 * * * *', async (req, res) => { ... });Option 1: Cron Expression (Recommended)
Use a cron expression to only run on weekdays:
// Every 15 minutes, Monday-Friday only
app.job('*/15 * * * 1-5', async (req, res) => {
// ... your logic
});Option 2: Conditional Logic
Add logic to check the day:
app.job('*/15 * * * *', async (req, res) => {
const dayOfWeek = new Date().getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
console.log('Weekend - skipping');
return res.end();
}
// ... rest of logic
});Cron expression reference:
*/15 * * * *- Every 15 minutes, every day*/15 * * * 1-5- Every 15 minutes, Monday-Friday only*/15 9-17 * * 1-5- Every 15 minutes, 9 AM-5 PM, weekdays only0 9 * * 1-5- Once at 9 AM, weekdays only
The email template is in a separate file for easy customization:
Edit email-template.js:
export function generateEmailTemplate({
subject,
heading,
body,
buttonText,
buttonUrl,
logoUrl,
fromName
}) {
return `
<!DOCTYPE html>
<html>
<!-- Customize HTML structure and CSS here -->
...
</html>
`.trim();
}Changes you can make:
- Modify CSS styles (colors, fonts, layout)
- Change HTML structure
- Add additional sections or elements
- Update gradient colors (currently purple gradient)
- Customize responsive breakpoints
After editing, redeploy:
coho deploySimply edit stepsconfig.json and add more step objects:
{
"workflowSteps": [
{ "step": 1, "hoursAfterSignup": 24, "template": { ... } },
{ "step": 2, "hoursAfterSignup": 96, "template": { ... } },
{ "step": 3, "hoursAfterSignup": 264, "template": { ... } },
{ "step": 4, "hoursAfterSignup": 720, "template": { ... } },
{ "step": 5, "hoursAfterSignup": 1440, "template": { ... } }
]
}Then redeploy:
coho deployNo code changes needed!
- Check env vars:
coho env list - Verify API keys are active
- Check logs:
coho logs --tail 50 - Test email provider directly
- Check workflow config:
curl https://your-project.api.codehooks.io/dev/ - Verify cron is running (look for logs every 15 min)
- Check if enough time has passed since
createdAt - Verify subscriber is
subscribed: true
The number of steps is configured in stepsconfig.json. Check your configuration:
curl https://your-project.api.codehooks.io/dev/To change, edit stepsconfig.json and redeploy:
coho deploy- 100 subscribers: Works perfectly with default config
- 1,000 subscribers: No changes needed, streaming handles this easily
- 10,000 subscribers: Still efficient with streaming architecture
- 100,000+ subscribers:
- Monitor email provider rate limits
- Consider running cron less frequently (every 30-60 min)
- Add database indexes on
subscribedandcreatedAtfields - Monitor queue processing times
The architecture scales extremely well because:
- Streaming data processing: Constant memory usage regardless of subscriber count
- Single cron job: Efficient time-based logic
- Queue-based delivery: Handles parallel processing automatically
- No in-memory arrays: Uses cursor.forEach() to process one record at a time
- Simple state management: Only tracks which steps have been sent
- Verify domain with SPF and DKIM
- Start small, gradually increase volume
- Monitor bounce rates
- Include unsubscribe link (already in templates)
- Never commit API keys
- Use API tokens for unsubscribe
- Validate email addresses (basic validation included)
- GDPR: Easy unsubscribe provided
- CAN-SPAM: Include physical address in footer
- CASL: Obtain explicit consent
MIT
Why This Architecture?
β
Simple: One cron job, one queue worker, one config file
β
Flexible: Unlimited steps via stepsconfig.json
β
Integrated: Templates and timing in one place
β
Intelligent: Automatic time-based scheduling
β
Scalable: Streaming architecture handles high numbers of subscribers with constant memory usage
β
Reliable: Queue retries, duplicate prevention
β
Maintainable: Easy to understand and debug
Perfect for drip campaigns, onboarding sequences, course delivery, and automated email marketing!
