Open source GPL-3.0 · self-hosted

Email automation you run yourself.

Basalt is an open-source alternative to Mautic that does one thing well: email. Contacts, dynamic segments, visual campaigns and tracking — on your servers, through the SMTP providers you already pay for.

git clone https://github.com/juliendu11/basalt.git

Sends through the SMTP you already pay for

  • Brevo
  • Mailgun
  • SendGrid
  • Amazon SES
  • Your own server

Contacts in. Campaigns out.

Screens from a running instance, seeded with Basalt’s demo data. Nothing here is a mockup.

A campaign is a graph, not a list

Drop a segment source, waits, sends and conditions on the canvas, then branch on whether the last email was opened or clicked.

  • Segment source
  • Waits that survive restarts
  • Draft · published · archived
Basalt campaign builder: an abandoned-cart flow going from a segment source, to a one-hour wait, to a send_email node, to an email_clicked condition that branches into add_tag (true) and a second send_email (false).

Every contact has an inbox history

What was already sent, with delivery status and first open — and what campaigns will send next.

  • subscribed
  • bounced
  • complained
A Basalt contact's email timeline: three past sends with bounced and delivered statuses, a Now marker, then one scheduled send.

Change the footer once

A layout is the shared frame around your content, injected at render time — one edit reaches every email that uses it.

  • {{ email_body }}
  • Plain-text frame
  • Sandboxed preview
Basalt email layout editor: an HTML table-based branding frame with an email_body placeholder and an unsubscribe_url link, variable buttons above it, and an optional plain-text version below.

Segments that keep themselves current

Nested AND/OR filters over standard and custom fields, a live match count, and membership recomputed when a contact changes.

  • AND / OR builder
  • Tags in bulk
  • Custom fields
Basalt contact list for the Boutique Lumen project, filterable by search, status and tag, with subscribed, complained, bounced and unsubscribed status badges.

Statistics per project and per campaign

Sends, deliveries, opens, clicks, bounces and unsubscribes — today, 7 days, 30 days or a custom range.

Project dashboard with counters for contacts, sends, deliveries, opens, clicks, bounces and unsubscribes, and an activity-over-time chart.

What it sent, what it will send

Each campaign lists its past sends with opens and clicks, next to its upcoming sends and their recipients.

Campaign email activity: a table of recently sent emails with contact, subject, send time, delivery status and opened or clicked badges.

One image. Three processes.

The same Docker build runs as web, queue or scheduler — pick with PROCESS_TYPE and point all of them at one MySQL/MariaDB and one Redis.

  1. PROCESS_TYPE=web

    web

    The Inertia app and the REST API. Runs pending migrations on start.

    N instances behind a load balancer

  2. PROCESS_TYPE=queue

    queue

    BullMQ workers for emails, campaign-engine, segments, tracking and statistics.

    Scale out, or split with QUEUE_NAMES

  3. PROCESS_TYPE=scheduler

    scheduler

    Nightly segment recompute, wait-step polling every 60 s, nightly stats roll-up.

    Run exactly one

production · docker
# build once
docker build -t basalt .

# HTTP server
docker run --env-file .env.production -p 3333:3333 -e PROCESS_TYPE=web basalt

# workers — scale independently
docker run --env-file .env.production -e PROCESS_TYPE=queue basalt

# exactly one scheduler
docker run --env-file .env.production -e PROCESS_TYPE=scheduler basalt

Generate APP_KEY once and keep it — it also derives the key that encrypts your SMTP passwords.

Where Basalt parts ways with Mautic

It keeps what teams rely on — contacts, segments, campaigns, templates, stats, an API — and drops the one-mail-server-for-everything model.

Topic Basalt Mautic
Sending One or more SMTP connectors per project — Brevo, Mailgun, SendGrid, SES or your own server — each with a default, an on/off switch and an optional daily limit. One global mail transport for the whole instance.
Credentials AES-256-GCM, key derived from APP_KEY. Write-only from the UI, decrypted only at send time. Kept in global config.
Sender identity senderName, senderEmail and replyTo on the connector and on each email. Instance-level “from” settings.
Layouts Injected at render time. Edit the layout, every email that uses it updates. A layout change means re-editing each template.
Translation Clone an email into another language — subject, preheader, HTML and text — with the HTML structure kept. Optional, via Google Translate. —
Tenancy Organizations and projects with strict data isolation; owner, admin, member and viewer roles; invitations. Single-tenant.
Campaign engine Persisted enrollments and executions. Idempotent jobs, retried with backoff, plus a failed-jobs screen to re-run them. —

Guarantees, not preferences

The parts that matter when the list is real and the queue is full.

  • idempotency_key

    Sends happen once

    One key per delivery and an atomic state change before the SMTP call. A retried job never sends twice.

    $ node ace queue:work --queue=emails
    send_email  execution 4812 · node n3
      check      contact is subscribed
      reserve    idempotency_key 4812:n3
      connector  Brevo · project default
      render     {{ contact.firstname }} → Eustache
      smtp       250 2.0.0 OK
    One send, traced (illustrative). A retry reuses the same key.
  • content_freeze

    Published means fixed

    Publishing a campaign version freezes each send’s subject, body and sender identity.

  • unsubscribe

    Checked on every send

    A per-contact token, one click, and an eligibility check before each send — not just a UI preference.

  • tracking

    Opens, clicks, bounces

    An open pixel, rewritten click links, and provider webhooks for bounces, complaints and deliveries.

  • failed_jobs

    Retries you can see

    Every job retries with backoff. What still fails lands on a per-project screen where you run it again.

  • audit_log

    Who changed what

    User actions recorded at organization and project scope.

  • sandbox

    Previews can’t run scripts

    Email HTML is previewed inside a sandboxed iframe srcdoc.

  • api/v1

    A REST API per project

    Token-authenticated, scoped to one project, rate-limited — and typed end to end from the Vue client.

Built on parts you already know

No PHP monolith, no plugin sprawl — a typed Node.js app and its workers.

  • Node.js ≥ 24
  • AdonisJS 7
  • Vue 3 + Inertia
  • Tailwind CSS v4
  • Vue Flow
  • Tuyau
  • BullMQ
  • Redis
  • MySQL / MariaDB
  • Lucid ORM
  • VineJS
  • Nodemailer
  • Docker

Read the code before you trust it with your list.

Basalt is free software under the GPL-3.0. Clone it, start the local stack with Docker Compose, and send your first campaign into Mailcatcher.