Open-Source Cold Email & Warmup Platform
Warmbly 是开源的冷邮件平台:温暖您的邮箱、发送活动、并对每条回复进行分流。可在我们的云端运行,也可自行托管整套系统。
AI 翻译
尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
Self-hosted email marketing automation, built on modern Node.js.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Self-hosted email marketing automation, rebuilt on modern Node.js.
Basalt is an open-source alternative to Mautic that focuses on doing one thing well: email. Contacts, dynamic segments, visual campaigns, templates, tracking and statistics — without the PHP monolith, the plugin sprawl, or the "one global mail server for everything" model.
It is a fresh implementation on AdonisJS 7 (Node.js ≥ 24), with a Vue 3 + Inertia SPA front-end, a typed API layer, and a Redis/BullMQ job system that runs the heavy work (sending, segmentation, campaign progression, stats) in dedicated worker processes.
Basalt deliberately keeps the parts of Mautic that teams actually rely on:
subscribed / unsubscribed /
bounced / complained / blocked), soft delete, per-contact history.today / 7d / 30d / custom ranges.| Basalt | Mautic |
|---|---|
| One or more SMTP connectors per project — Brevo, Mailgun, SendGrid, SES, custom server — each with its own credentials, default connector, enable/disable toggle and optional daily send limit. | A single global mail transport for the whole instance. |
Encrypted SMTP credentials (AES-256-GCM, key derived from APP_KEY), write-only from the UI, decrypted only at send time — never sent back to the browser. | Credentials in global config. |
Per-project sending identity — senderName, senderEmail and replyTo configurable on both the SMTP connector and the individual email. | Instance-level "from" settings. |
Live email layouts — a shared HTML branding frame (header/footer/wrapper) with an {{ email_body }} placeholder. Editing a layout instantly updates every email that references it — the content is injected at render time, never copied. | Layout changes mean re-editing each template. |
| One-click email translation — clone an email machine-translated into another language (subject, preheader, HTML body, plain text), HTML structure preserved. Optional, via the Google Translate API. | — |
Multi-organization, multi-project with strict data isolation and per-org roles (owner / admin / member / viewer) and invitations. | Single-tenant. |
A reliable campaign engine — enrollments and executions are persisted; wait steps survive restarts (a scheduler polls for due executions); every job is idempotent and retried with backoff, with a failed-jobs UI to inspect and re-run. | — |
send_email node gets a frozen copy of the
subject/body/identity. Editing an email (or its layout) afterwards can never change what an already-running campaign
sends.idempotency_key per delivery plus an atomic state transition before the SMTP call
means a retried job never double-sends.<iframe srcdoc>.{{ namespace.field }} variable renderer for email contentThe same build is deployed as up to three process types, all pointed at the same database and Redis. The Docker
entrypoint selects one via the PROCESS_TYPE env var.
| Process | PROCESS_TYPE | Role | Scaling |
|---|---|---|---|
| web | web (default) | HTTP server — the Inertia SPA and the public REST API. Runs pending migrations on start. | N instances behind a load balancer. |
| queue | queue | BullMQ workers consuming the emails, campaign-engine, segments, tracking and statistics queues. Can consume a subset via QUEUE_NAMES. | Scale horizontally; optionally one dedicated worker per queue (e.g. a emails-only worker). |
| scheduler | scheduler | Periodic tasks: nightly full segment recompute (03:00 UTC), campaign wait-node due-execution polling (every 60s), nightly statistics pre-aggregation (04:00 UTC). | Run exactly one instance. |
The dev database, cache and mail-catching tools are provided by Docker Compose:
docker compose -f docker-compose.dev.yml up -d
This starts:
| Service | Purpose | Port |
|---|---|---|
| MariaDB | main database | 3306 |
| phpMyAdmin | DB admin UI | 8082 |
| Redis | queues + rate limiter | 6379 |
| Mailcatcher | catches outgoing emails, web UI | 1080 (UI) / 1025 (SMTP) |
| push-notification-catcher | catches push notifications | 6555 |
Copy the example env file and adjust it if needed (the defaults already match the Compose services above):
cp .env.example .env
Generate an APP_KEY if .env doesn't have one yet:
node ace generate:key
Fill in DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_DATABASE to match the MariaDB container (e.g. localhost /
3306 / root / root / app), and leave SMTP_HOST=localhost /
SMTP_PORT=1025 to send mail through Mailcatcher.
GOOGLE_TRANSLATE_API_KEY is optional — set it only to use the "clone email into another language" feature.
npm install
node ace migration:run
node ace migration:run regenerates database/schema.ts — never edit that file by hand, add or change columns via a
new migration instead.
npm run dev
This starts the AdonisJS server with HMR (node ace serve --hmr) at http://localhost:3333.
Two long-lived processes are separate from the HTTP server:
node ace queue:work # consumes all queues (emails, campaign-engine, segments, tracking, statistics)
node ace queue:work --queue=emails,tracking # or a subset
node ace scheduler:run # runs periodic tasks (see start/scheduler.ts)
Tests run against their own database (.env.test sets DB_DATABASE=app_test), kept empty between
tests by a per-test transaction — some specs assert on unscoped table counts, so they must never see
seeded or real data. Create and migrate it once:
docker compose -f docker-compose.dev.yml exec mariaDB \
mariadb -uroot -proot -e "CREATE DATABASE IF NOT EXISTS app_test"
NODE_ENV=test node ace migration:run
node ace test # all suites
node ace test unit # one suite: unit | functional | browser
node ace test --files="tests/functional/some.spec.ts"
npm run lint
npm run typecheck
npm run format
A multi-stage Dockerfile builds the app (node ace build) and produces a slim production image (production deps
only). docker/entrypoint.sh picks which process to run based on the
PROCESS_TYPE env var:
docker build -t basalt .
Set these on the container (see .env.example for the full list):
NODE_ENV=production, PORT, HOST, LOG_LEVEL, APP_KEY, APP_URL, TZPROCESS_TYPE: web (default), queue, or scheduler — selects what the entrypoint runsQUEUE_NAMES (queue processes only, optional): comma-separated subset of queues to consume; defaults to allSESSION_DRIVERDB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_DATABASELIMITER_STORE, REDIS_HOST, REDIS_PORT, REDIS_PASSWORDMAIL_MAILER, MAIL_FROM_NAME,
MAIL_FROM_ADDRESS, SMTP_HOST, SMTP_PORTGOOGLE_TRANSLATE_API_KEY (email translation)QUEUE_EMAILS_CONCURRENCY,
QUEUE_CAMPAIGN_ENGINE_CONCURRENCY, QUEUE_SEGMENTS_CONCURRENCY, QUEUE_TRACKING_CONCURRENCY,
QUEUE_STATISTICS_CONCURRENCYAPP_KEY must be a stable secret generated once (node ace generate:key) and reused across deploys/instances — it also
derives the key that encrypts SMTP credentials, so regenerating it per deploy makes existing sessions, encrypted data
and stored SMTP passwords unreadable.
The same image is deployed as (at least) three process types, each pointed at the same MySQL/MariaDB and Redis instances:
# HTTP server
docker run --env-file .env.production -p 3333:3333 -e PROCESS_TYPE=web basalt
# Queue worker(s) — scale independently of the web process
docker run --env-file .env.production -e PROCESS_TYPE=queue basalt
docker run --env-file .env.production -e PROCESS_TYPE=queue -e QUEUE_NAMES=emails,tracking basalt
# Scheduler — run exactly one instance
docker run --env-file .env.production -e PROCESS_TYPE=scheduler basalt
Only one scheduler process should run at a time; queue workers can be scaled horizontally, optionally split by queue
name for isolation (e.g. a dedicated worker for emails).
The web entrypoint runs node ace migration:run --force on start. To run them as a separate one-off step instead:
docker run --env-file .env.production --rm basalt node ace migration:run --force
Basalt is a modular monolith: one AdonisJS codebase, one database schema, one build — split into domains
(organizations, projects, contacts, segments, smtp, emails, campaigns,
automation, tracking, statistics, jobs) rather than technical layers. Async work runs on BullMQ queues consumed
by worker processes from the same repository. Domains talk to each other through an internal event bus; listeners
enqueue jobs rather than doing heavy work inline.
The design decisions behind each domain are documented in docs/plans/ — start with
docs/plans/00-overview.md and docs/plans/01-architecture.md, and see
docs/plans/14-jobs-and-queues.md for the queue/scheduler architecture and
docs/plans/decisions/ for the structural trade-offs (campaign graph storage, segment membership, campaign versioning,
email idempotency, queue system).
Basalt is free software, licensed under the GNU General Public License v3.0. See
LICENSE for the full text.
Copyright (C) 2026 Basalt contributors.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.