Jake Groszewski

Blog

Self-Host Your Own Customer Engagement Stack with Dittofeed: A Weekend Project for Developers

At some point, you've probably looked at your monthly SaaS bill and noticed that the customer engagement line item, whether that's Customer.io, OneSignal, or something similar, has quietly become one of the more expensive things you're paying for. And you're not getting raw data ownership out of it. Your user events, behavioral triggers, and contact lists are sitting on someone else's servers, subject to their pricing changes and their privacy policies.

Dittofeed is an open-source, omni-channel customer engagement platform that lets you run the whole thing yourself. Email, SMS, mobile push, WhatsApp, Slack, and other channels, all orchestrated from a single self-hosted backend. This is a realistic weekend project if you already have a VPS and a Django or Python app you want to wire it to.

What Dittofeed Actually Does

Think of it as a self-hosted alternative to Customer.io or Segment Engage. You define user segments, build message templates, set up journey workflows, and Dittofeed handles the delivery logic across whatever channels you configure. It's designed with developers in mind, meaning you get APIs and config-driven workflows rather than being forced through a no-code GUI for everything.

The platform handles both transactional messages (password resets, order confirmations) and lifecycle marketing campaigns (onboarding sequences, re-engagement flows) from the same installation. That's a useful consolidation if you've historically used one tool for transactional email and another for marketing.

Because everything runs in your own infrastructure, customer data stays there too. For teams dealing with GDPR, HIPAA-adjacent requirements, or just a healthy skepticism of third-party data pipelines, that matters.

Standing Up Dittofeed on a VPS

Dittofeed ships with Docker Compose, which makes the initial setup approachable. You'll want a VPS with at least 2 GB of RAM. A $12/month Hetzner or DigitalOcean instance works fine for a small app.

First, clone the repo and copy the example environment file:

git clone https://github.com/dittofeed/dittofeed.git
cd dittofeed
cp .env.example .env

Open .env and fill in the required values. The critical ones at minimum are your database connection string, a secret key, and your email provider credentials (Dittofeed doesn't send email itself; it calls your SMTP relay or an API like SendGrid or Postmark).

# .env (abbreviated)
DATABASE_URL=postgresql://user:password@localhost:5432/dittofeed
SECRET_KEY=your-long-random-secret
SMTP_HOST=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USER=your-postmark-api-token
SMTP_PASS=your-postmark-api-token

Then bring it up:

docker compose up -d

The web dashboard will be available on port 3000 by default. Put Nginx in front of it with a Let's Encrypt cert before you call it production-ready.

A basic Nginx config looks like this:

server {
    listen 80;
    server_name engage.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name engage.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/engage.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/engage.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Run certbot --nginx -d engage.yourdomain.com and you're done with the TLS setup.

Wiring It to a Django Backend

Dittofeed exposes a REST API for identifying users and tracking events, which maps directly onto what you'd do with a Segment-compatible analytics library. From Django, you can call it with httpx or plain requests.

Here's a utility module that covers the two most common calls you'll make:

# engagement/client.py
import httpx
from django.conf import settings

DITTOFEED_BASE_URL = settings.DITTOFEED_BASE_URL  # e.g. "https://engage.yourdomain.com"
DITTOFEED_API_KEY = settings.DITTOFEED_API_KEY

HEADERS = {
    "Authorization": f"Bearer {DITTOFEED_API_KEY}",
    "Content-Type": "application/json",
}


def identify_user(user_id: str, traits: dict) -> None:
    """Upsert a user and their properties into Dittofeed."""
    payload = {
        "userId": user_id,
        "traits": traits,
    }
    with httpx.Client() as client:
        response = client.post(
            f"{DITTOFEED_BASE_URL}/api/public/identify",
            json=payload,
            headers=HEADERS,
            timeout=5.0,
        )
        response.raise_for_status()


def track_event(user_id: str, event: str, properties: dict = None) -> None:
    """Send a behavioral event to Dittofeed."""
    payload = {
        "userId": user_id,
        "event": event,
        "properties": properties or {},
    }
    with httpx.Client() as client:
        response = client.post(
            f"{DITTOFEED_BASE_URL}/api/public/track",
            json=payload,
            headers=HEADERS,
            timeout=5.0,
        )
        response.raise_for_status()

Then call these from Django signals or view logic:

# In a post_save signal or a view
from engagement.client import identify_user, track_event

# After a user signs up
identify_user(
    user_id=str(user.id),
    traits={
        "email": user.email,
        "name": user.get_full_name(),
        "plan": user.profile.plan,
        "created_at": user.date_joined.isoformat(),
    },
)

# After a specific action
track_event(
    user_id=str(user.id),
    event="subscription_upgraded",
    properties={"new_plan": "pro", "price": 49},
)

With those two primitives in place, you can build any segment or journey in the Dittofeed dashboard. A user who triggers subscription_upgraded gets a welcome email. A user whose last_active_at trait is more than 30 days old enters a re-engagement flow. The logic lives in Dittofeed; your Django app just reports what's happening.

A Few Things Worth Planning Around

Dittofeed is actively developed, but it's still maturing. The journey builder covers the main use cases well, though you'll occasionally hit a rough edge in the UI. Keep that in mind if you're building something that a non-technical teammate needs to operate.

For backups, Postgres is doing most of the heavy lifting, so set up automated pg_dump exports to object storage (Backblaze B2 is cheap and works fine). Don't assume the Docker volumes are enough.

If you're sending marketing email at any real volume, your VPS IP reputation matters. Use a dedicated SMTP relay with proper SPF, DKIM, and DMARC records. Dittofeed handles the send logic; deliverability is still your problem to own.

One more thing: Dittofeed supports multiple channels from a single install, so you can add SMS via Twilio or push notifications later without standing up separate infrastructure. The omni-channel setup is worth thinking through upfront even if you only need email on day one.

Actionable Takeaways

  • Spin up Dittofeed using Docker Compose on a 2 GB VPS and put it behind Nginx with TLS before treating it as production
  • Use the identify and track API endpoints from Django to feed user properties and behavioral events into the platform
  • Build segments and journeys in the Dittofeed dashboard based on the traits and events your backend sends
  • Back up Postgres on a schedule and route all email through a reputable SMTP relay with proper DNS records
  • Plan your channel configuration upfront even if you're starting with email only, since adding SMS or push later is much easier if the foundation is already there

If you've been meaning to get off a per-seat or per-message SaaS plan and actually own your engagement data, pick a weekend to do it. The setup time is real but not unreasonable, and the result is a platform you control completely.