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
Open source GPL-3.0 · self-hosted
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
Screens from a running instance, seeded with Basalt’s demo data. Nothing here is a mockup.
Drop a segment source, waits, sends and conditions on the canvas, then branch on whether the last email was opened or clicked.
What was already sent, with delivery status and first open — and what campaigns will send next.
A layout is the shared frame around your content, injected at render time — one edit reaches every email that uses it.
{{ email_body }}
Nested AND/OR filters over standard and custom fields, a live match count, and membership recomputed when a contact changes.
Sends, deliveries, opens, clicks, bounces and unsubscribes — today, 7 days, 30 days or a custom range.
Each campaign lists its past sends with opens and clicks, next to its upcoming sends and their recipients.
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.
PROCESS_TYPE=web
The Inertia app and the REST API. Runs pending migrations on start.
N instances behind a load balancer
PROCESS_TYPE=queue
BullMQ workers for emails, campaign-engine,
segments, tracking and statistics.
Scale out, or split with QUEUE_NAMES
PROCESS_TYPE=scheduler
Nightly segment recompute, wait-step polling every 60 s, nightly stats roll-up.
Run exactly one
# 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.
It keeps what teams rely on — contacts, segments, campaigns, templates, stats, an API — and drops the one-mail-server-for-everything model.
APP_KEY. Write-only from the UI, decrypted only at send time.
Kept in global config.
senderName, senderEmail and replyTo on the connector and on each email.
Instance-level “from” settings.
The parts that matter when the list is real and the queue is full.
idempotency_key
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
content_freeze
Publishing a campaign version freezes each send’s subject, body and sender identity.
unsubscribe
A per-contact token, one click, and an eligibility check before each send — not just a UI preference.
tracking
An open pixel, rewritten click links, and provider webhooks for bounces, complaints and deliveries.
failed_jobs
Every job retries with backoff. What still fails lands on a per-project screen where you run it again.
audit_log
User actions recorded at organization and project scope.
sandbox
Email HTML is previewed inside a sandboxed iframe srcdoc.
api/v1
Token-authenticated, scoped to one project, rate-limited — and typed end to end from the Vue client.
No PHP monolith, no plugin sprawl — a typed Node.js app and its workers.
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.
No match. Try “docker” or “mautic”.