Open source, self-hostable email drip engine for developers.
Stop writing cron jobs. Define email sequences as YAML, enroll contacts with 3 lines of code, and let DripEngine handle the scheduling, conditions, retries, and delivery — on your own infrastructure.
import { DripEngine } from '@dripengine/sdk'
const drip = new DripEngine({ apiKey: process.env.DRIP_KEY })
await drip.enroll({
sequenceId: 'onboarding',
contact: { email: 'john@gmail.com', plan: 'free' }
})Every SaaS needs drip emails. Most teams end up with the same mess — a cron job querying the database, an if/else chain checking conditions, a retry function that silently fails, and zero visibility into what actually sent.
DripEngine replaces all of that with a config file and an SDK call.
| DIY approach | DripEngine | |
|---|---|---|
| Setup time | Days | 2 minutes |
| Time-based delays | Cron jobs | delay: 5d in YAML |
| Condition branching | If/else spaghetti | Declarative conditions |
| Retries | Hope | Exponential backoff |
| Visibility | Grep logs | Dashboard |
| Self-hostable | Yes (you built it) | Yes (one command) |
- Sequence-as-code — define entire email flows in YAML. Version control your sequences like any other config.
- Condition branching — skip or cancel steps based on contact properties or events they've fired. No code required.
- Smart cancellation — when a contact upgrades or completes an action, pending emails that are no longer relevant are automatically cancelled.
- Any SMTP provider — works with Nodemailer, Resend, SendGrid, SES, Postmark, or any raw SMTP server.
- Dashboard included — built-in Next.js dashboard for sequence health, contact timelines, and delivery logs.
- Self-hostable — runs entirely on your infra with one
docker-compose up. No vendor lock-in, no usage limits. - Cloud option — don't want to manage infra? Use our hosted version at dripengine.dev.
Prerequisites: Docker Desktop
git clone https://github.com/nirvanjha2004/dripengine.git
cd dripengine
cp .env.example .env # fill in your SMTP credentials
docker-compose upThat's it. Your API is running at http://localhost:8000 and your dashboard at http://localhost:3000.
npm install @dripengine/sdkCreate a YAML file in the sequences/ folder:
# sequences/onboarding.yaml
id: onboarding
trigger: user.signup
steps:
- id: welcome
delay: 0
template: welcome-email
- id: tip-day2
delay: 1d
template: first-tip
condition:
event_not_fired: user.completed_profile # skip if they already did this
- id: tip-day5
delay: 5d
template: second-tip
condition:
property: plan
equals: free # skip if they upgraded
- id: upgrade-nudge
delay: 10d
template: upgrade
condition:
property: plan
equals: freeNo restart needed — sequences are hot-reloaded.
import { DripEngine } from '@dripengine/sdk'
const drip = new DripEngine({
apiKey: process.env.DRIP_KEY,
baseUrl: 'http://localhost:8000', // or your deployed URL
})
// Enroll a contact when they sign up
await drip.enroll({
sequenceId: 'onboarding',
contact: {
email: 'john@gmail.com',
name: 'John',
timezone: 'Asia/Kolkata',
properties: { plan: 'free' }
}
})
// Fire an event when something happens in your app
// The engine will automatically cancel steps gated on this event
await drip.event({
eventName: 'user.completed_profile',
email: 'john@gmail.com'
})
// Unenroll when they upgrade — stops all pending emails
await drip.unenroll({
sequenceId: 'onboarding',
email: 'john@gmail.com'
})Or use the REST API directly:
curl -X POST http://localhost:8000/enroll \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"sequence_id": "onboarding",
"contact": {
"email": "john@gmail.com",
"properties": { "plan": "free" }
}
}'Templates live in the templates/ folder. Each template is a folder with three files:
templates/
welcome-email/
subject.txt ← subject line (supports {{variables}})
html.hbs ← HTML body (Handlebars)
text.hbs ← plain text fallback
Example html.hbs:
<h1>Hey {{name}}</h1>
<p>Welcome! You're on the <strong>{{plan}}</strong> plan.</p>Variables are automatically filled from the contact's properties.
Conditions are evaluated at send time — not at schedule time. The engine always checks the contact's current state.
# Send only if a property matches
condition:
property: plan
equals: free
# Send only if a property does NOT match
condition:
property: plan
not_equals: paid
# Send only if an event has NOT been fired
condition:
event_not_fired: user.completed_profile
# Send only if an event HAS been fired
condition:
event_fired: user.invited_teammateSet PROVIDER in your .env:
# Nodemailer — any SMTP server (default)
PROVIDER=nodemailer
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASS=your-app-password
# Resend
PROVIDER=resend
RESEND_API_KEY=re_xxxx
# SendGrid
PROVIDER=sendgrid
SENDGRID_API_KEY=SG.xxxxThe built-in dashboard runs at http://localhost:3000:
- Overview — total contacts, active enrollments, emails sent today
- Sequences — per-sequence health with sent / skipped / failed breakdown
- Contacts — search any email and see their full timeline
- Logs — paginated delivery log with filtering by status and sequence
┌─────────────────────────────────────────────────┐
│ Ingestion layer │
│ REST API · SDK · Webhook triggers · CLI import │
└──────────────────────┬──────────────────────────┘
│
┌──────────────────────▼──────────────────────────┐
│ Core engine │
│ Scheduler · Condition evaluator · Retry manager │
│ Template renderer · Contact state (Postgres) │
│ Queue: Redis + BullMQ │
└──────────────────────┬──────────────────────────┘
│
┌──────────────────────▼──────────────────────────┐
│ Delivery layer │
│ Nodemailer · Resend · SendGrid · SES │
└─────────────────────────────────────────────────┘
Stack: FastAPI · Node.js · BullMQ · Redis · Postgres · Next.js · Docker
| Variable | Required | Description |
|---|---|---|
API_SECRET_KEY |
Yes | Auth key for all API requests |
DATABASE_URL |
Yes | Postgres connection string |
REDIS_HOST |
Yes | Redis host |
PROVIDER |
Yes | Email provider: nodemailer, resend, sendgrid |
SMTP_HOST |
If nodemailer | SMTP server host |
SMTP_USER |
If nodemailer | SMTP username |
SMTP_PASS |
If nodemailer | SMTP password / app password |
RESEND_API_KEY |
If resend | Resend API key |
SENDGRID_API_KEY |
If sendgrid | SendGrid API key |
The worker is stateless and horizontally scalable. To handle more volume, spin up more worker containers — BullMQ ensures no job is processed twice:
worker:
deploy:
replicas: 3Full interactive docs available at http://localhost:8000/docs after starting the server.
| Method | Endpoint | Description |
|---|---|---|
POST |
/enroll |
Enroll a contact into a sequence |
POST |
/event |
Fire an event for a contact |
POST |
/unenroll |
Remove a contact from a sequence |
GET |
/dashboard/overview |
Summary stats |
GET |
/dashboard/sequences |
Per-sequence health |
GET |
/dashboard/contacts |
Contact timeline |
GET |
/dashboard/logs |
Delivery log feed |
GET |
/health |
Health check |
- Webhook ingestion (Stripe, GitHub, etc.)
- A/B testing for email templates
- Unsubscribe link handling
- Multi-tenant support
- Hosted cloud version
PRs are welcome. Please open an issue first for anything beyond small fixes.
git clone https://github.com/nirvanjha2004/dripengine.git
cd dripengine
cp .env.example .env
docker-compose upThe API hot-reloads on save. The dashboard hot-reloads on save. The worker requires a restart on code changes.
MIT — use it, fork it, build on it.
Built by @nirvanjha2004 · Report a bug · Request a feature