Three surfaces close the loop on the limit-increase workflow:
- admin/dashboard/LimitRequestsPage.tsx queues every pending request
with full context (org → users → field → current vs requested →
+delta) and one-click approve/reject. Both actions open a review
dialog; approve notes are optional, reject notes are required and
surface to the customer.
- web/settings/limits/page.tsx is the customer-facing form. Resource
selector, requested value, reason textarea, plus a list of every
past request with its status (pending/approved/rejected/cancelled)
and the reviewer's notes when present. Pending rows expose a
cancel link. Footer links to the ToS limits clause.
- site/terms.astro grows a new section 07 ("Usage limits and
increase requests"). Explicit: "unlimited" means no plan-tier cap
but a product-wide hard ceiling still applies, increases are at
Warmbly's sole discretion, and previously granted increases can be
revoked when reputation signals deteriorate. Bumps every existing
section heading and id from 07 onward.
Admin sidebar grows a "Limit requests" entry under Accounts (Gauge
icon). Web settings layout grows a "Limits" section under owner-only
sections.
Warmbly
The open-source cold email and mailbox warmup platform you can actually self-host.
No AWS lock-in. Distributed senders. Multi-IP workers. Admin UI included.
Features · Quick start · Architecture · Self-hosting · Admin UI · No lock-in
Why Warmbly
Most cold email platforms force you into one of two corners. Hosted SaaS — fast to start, but your sender reputation lives in someone else's IP pool, and your data lives in someone else's database. Or roll-your-own — full control, six months of plumbing before you send the first email.
Warmbly is the third option: a real cold-outreach platform that runs on your infrastructure, your IPs, your database, without making you give up the features you'd expect from a hosted SaaS.
You can run it on a single $5 VPS with SQLite-free Postgres. You can run it across a Hetzner CX32 fleet with 16 IPs per box. The same code handles both.
Features
🛡️ Self-hostableSingle binary per service. No required calls to AWS, GCP, Stripe, or Cloudflare. Postgres + Redis + NATS is the whole infrastructure stack. |
🌐 Distributed workersOne worker per IP, many workers per VPS. Multi-IP install in one command. Reputation per IP, not per VPS. |
🔌 Pluggable everythingSwap KMS, blob store, event bus, codec at deploy time. AWS or local AES. Kafka or NATS JetStream. S3 or filesystem. |
🎛️ Admin UIProduction-grade React + Vite admin app with workers, egresses, mailboxes, warmup, audit, and settings. |
🔐 Envelope encryptionKMS-wrapped per-user DEKs. Workers fetch them over HTTPS, never touch Postgres directly. Constant-time bearer-token auth on internal endpoints. |
✉️ Real warmupPool-based warmup with anti-abuse signals, spam-score tracking, and auto-blocking on token-forgery patterns. Free vs premium pool isolation. |
📊 Multi-tier worker fleetShared free, shared premium, dedicated per-org. Tier migrations with mailbox rebalancing. Per-egress health bands. |
🧪 Mailbox-first safetyPer-mailbox send caps (default 50/day), spacing (default 600s), warmup ramp from 10 to 40/day. Worker volume = sum of mailbox budgets, not a flat per-worker limit. |
🛰️ Real-time trackingOpen/click pixel + redirect service (Rust), with deduplication at both the tracker and the consumer level. Replay-resistant. |
Quick start
The fastest path to a running stack — one VPS, one command, everything local:
# Clone, install deps, boot everything
git clone https://github.com/warmbly/warmbly && cd warmbly
make infra # Postgres, Redis, NATS, Kafka, Schema Registry, MailHog (~1 min)
make app # backend, consumer, worker, tracking, realtime, dashboard (~30s)
# Open the dashboard
open http://localhost:5173
The admin app and marketing site live outside the compose stack and run on demand from their own terminals:
make admin # Vite dev server → http://localhost:5174
make site # Astro dev server → http://localhost:4321
To open the admin app you need an account with admin_permissions > 0.
There is no way to bootstrap the first admin from inside the UI, so sign
up normally through the dashboard, then promote yourself from the host:
make grant-admin EMAIL=you@example.com # super (all perms)
make grant-admin EMAIL=you@example.com ROLE=support # support|ops|analyst
make revoke-admin EMAIL=you@example.com # back to 0
That's it. Hot reload is wired for every language service (Go, Rust, Elixir, React).
For production: see Self-hosting below.
Admin UI
The admin app lives at admin/, separate from the user dashboard, with a
clear amber accent so admins never confuse the two.
What it covers:
- Overview — fleet health, mailbox counts, send rates, bounce rates
- Workers — physical worker processes, SSH lifecycle (test / install / restart / upgrade / logs)
- Egresses — sending identities (worker × IP), health scores, quarantine
- Mailboxes — every connected mailbox across the platform
- Warmup — pools, participants, blocked accounts, appeals
- Settings — KMS / blob store / encrypted-keys / event bus / cache / transports — all selectable backends
- Audit log — every admin action with diffs
Architecture
┌─────────────────────────────────┐
│ Admin UI │
│ (React + Vite app) │
└────────────────┬────────────────┘
│
▼
┌────────────────┐ HTTPS ┌─────────────────────────────────────┐
│ Dashboard │ ◄───────► │ Backend API │
│ (React) │ │ (Go, Gin, REST) │
└────────────────┘ │ │
│ ┌──────────┐ ┌────────────────┐ │
│ │ Settings │ │ /api/v1/ │ │
│ │ Registrar│ │ internal/dek │ │
│ └──────────┘ └────────┬───────┘ │
└──┬────────────────┬────┴────────────┘
│ │ │
▼ ▼ │
┌─────────────────┐ ┌──────────┐ │
│ PostgreSQL │ │ Redis │ │
│ users, accounts │ │ cache │ │
│ DEKs (via PG) │ │ ratelim │ │
└─────────────────┘ └──────────┘ │
│
┌──────────────────────────────────────┘
│
▼ Kafka / NATS JetStream events
┌─────────────────────────────────────────────────────────────────┐
│ Distributed worker fleet │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ Worker N │ ... │
│ │ IP A │ │ IP B │ │ IP C │ │ IP X │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴──────────────┘ │
│ │ │
│ ▼ │
│ Outbound SMTP / IMAP / OAuth APIs │
│ (Gmail, Microsoft 365, Zoho, custom) │
└─────────────────────────────────────────────────────────────────┘
Control plane: backend API + consumer + Postgres + Redis + event bus. Decides what to send and where; owns all stateful data.
Execution plane: distributed worker fleet. One Go binary per VPS, one worker process per IP. Workers receive commands over the event bus, fetch DEKs over HTTPS, and emit telemetry back. Workers never connect to Postgres.
The split matters because workers are intended to scale horizontally across many cheap VPSes — each one is a sending identity, not a database client.
Self-hosting
Warmbly is designed so a self-hoster never has to pay AWS / GCP / Cloudflare / Stripe for anything except the boxes they want to rent.
| Concern | Self-host default | Cloud option |
|---|---|---|
| Database | PostgreSQL 16 | RDS / Cloud SQL |
| Cache | Redis (or Valkey) | ElastiCache |
| Event bus | NATS JetStream (1 binary) | Kafka, MSK |
| Blob storage | Filesystem | S3, MinIO, R2, B2 |
| KMS / root key | Local AES master key | AWS KMS, Vault, GCP |
| Encrypted DEKs | PostgreSQL table | DynamoDB / Scylla |
| Codec | JSON | Avro + Schema Registry |
| Captcha | Bypass token (trusted) | Cloudflare Turnstile |
| Payments | Off | Stripe |
Every adapter is selected via an env var. See docs/VENDOR_LOCKIN.md for the honest audit of every external dependency and what to do about it.
Minimum-viable env
# Self-hostable defaults
KMS_PROVIDER=local
KMS_LOCAL_MASTER_KEY=$(openssl rand -base64 32)
ENCRYPTED_KEYS_PROVIDER=postgres # on the backend
INTERNAL_API_TOKEN=$(openssl rand -base64 32)
BLOB_PROVIDER=filesystem
BLOB_FS_ROOT=/var/lib/warmbly/blobs
EVENTBUS_PROVIDER=nats
NATS_URL=nats://localhost:4222
CODEC_PROVIDER=json
Workers get one extra:
ENCRYPTED_KEYS_PROVIDER=http
ENCRYPTED_KEYS_BACKEND_URL=https://api.yourdomain.com
ENCRYPTED_KEYS_WORKER_TOKEN=<same as INTERNAL_API_TOKEN>
Multi-IP worker installation
One Hetzner CX32 with 16 attached Primary IPs becomes 16 sending identities with one command:
sudo ./scripts/install-worker.sh \
--kafka kafka.yourdomain.com:9092 \
--redis redis://cache.yourdomain.com:6379 \
--ips 5.6.7.11,5.6.7.12,5.6.7.13,5.6.7.14,5.6.7.15,\
5.6.7.16,5.6.7.17,5.6.7.18,5.6.7.19,5.6.7.20,5.6.7.21,\
5.6.7.22,5.6.7.23,5.6.7.24,5.6.7.25,5.6.7.26
Each IP gets its own systemd unit (warmbly-worker@<dashed-ip>.service) and
a deterministic UUIDv5 identity — reputation persists across reinstalls.
Full runbook in docs/MULTI_IP_WORKERS.md.
Stack
| Layer | Tech | Why |
|---|---|---|
| Backend API | Go 1.25 + Gin | Same binary on a Pi, an EC2 box, or a Hetzner dedicated |
| Consumer | Go (event-bus driven) | Same |
| Worker | Go (Kafka/NATS subscriber) | ~50MB RAM per process; runs anywhere with port 25 open |
| Tracking | Rust + Axum | Open/click pixel hot path; dedup in-memory + persistent |
| Realtime | Elixir + Phoenix Channels | WebSocket fanout, naturally concurrent |
| Dashboard | React 19 + Vite + Tailwind v4 + shadcn | Modern stack, no Next.js coupling |
| Admin UI | React 19 + Vite + Tailwind v4 + shadcn | Same stack as dashboard; distinct visual identity |
| Primary DB | PostgreSQL 16 | Boring, durable, well-known |
| Cache | Redis 7 (or Valkey / KeyDB) | Sliding-window rate limits + DEK cache |
| Event bus | NATS JetStream (default) or Kafka | NATS = single binary; Kafka = the historical hosted path |
Project layout
warmbly/
├── cmd/
│ ├── backend/ # REST API + admin orchestration
│ ├── consumer/ # event-bus consumer → Postgres
│ ├── worker/ # distributed sender (one per IP)
│ └── seed/ # local-dev fixtures
├── internal/
│ ├── api/ # HTTP handlers, routes, middleware
│ ├── app/ # business services (auth, email, campaign, ...)
│ ├── client/
│ │ ├── netbind/ # per-egress bind-IP for SMTP/IMAP
│ │ └── smtpimap/ # SMTP + IMAP client
│ ├── events/ # publisher + event schemas
│ ├── infrastructure/
│ │ ├── codec/ # Avro + JSON (pluggable)
│ │ ├── encryptedkeys/ # Postgres + DynamoDB + HTTP (pluggable)
│ │ ├── eventbus/ # Kafka + NATS JetStream (pluggable)
│ │ ├── kms/ # local + AWS (pluggable)
│ │ └── storage/ # filesystem + S3-compatible (pluggable)
│ ├── models/ # domain types
│ └── repository/ # Postgres data access
├── tracking/ # Rust open/click service
├── realtime/ # Elixir WebSocket gateway
├── web/ # User dashboard (Vite + React)
├── admin/ # Admin UI (Vite + React)
├── scripts/
│ └── install-worker.sh # single-IP + multi-IP installer
├── docs/ # operator docs (auth, lock-in, multi-IP)
└── resources/ # technical / architectural docs
Building
go build -o bin/backend ./cmd/backend
go build -o bin/consumer ./cmd/consumer
go build -o bin/worker ./cmd/worker
cd tracking && cargo build --release
cd realtime && mix deps.get && mix release
cd web && pnpm install && pnpm build
cd admin && pnpm install && pnpm build
Testing
go test ./...
cd web && pnpm typecheck && pnpm lint
cd admin && pnpm typecheck && pnpm lint
cd tracking && cargo test
cd realtime && mix test
Coverage highlights:
internal/infrastructure/codec— 11 tests (JSON round-trip + Avro interface conformance)internal/infrastructure/encryptedkeys— 14 tests (HTTP round-trip, factory, conflict semantics)internal/infrastructure/eventbus— 16 tests (Kafka + NATS round-trip + ack redelivery)internal/infrastructure/kms— 11 tests (local AES round-trip + tamper detection + factory)internal/infrastructure/storage— 14 tests (filesystem + traversal protection + factory)internal/client/netbind— 4 tests (dialer + TLS dialer + env fallback)internal/api/middleware— 5 internal-auth testsinternal/api/handler— 9 internal-DEK handler testsinternal/app/settings— 5 registrar testscmd/worker— 5 UUID-from-IP derivation tests
Full suite runs in under 5 seconds.
Documentation
| Doc | What it covers |
|---|---|
| docs/VENDOR_LOCKIN.md | Every external dependency and how to replace it |
| docs/INTERNAL_API_AUTH.md | How workers authenticate to the backend |
| docs/MULTI_IP_WORKERS.md | Hetzner CX32 + 16 Primary IPs deployment recipe |
| resources/architecture.md | Control-plane vs execution-plane split, encryption model |
| resources/local-development.md | Docker Compose, profiles, seeding |
| resources/deployment-guide.md | Production control plane + worker fleet |
| resources/Events.md | Event bus event reference |
| resources/EMSG.md | Encrypted-message blob format |
Security
If you find a vulnerability please email security@warmbly.com rather than
opening a public issue. We prefer responsible disclosure and will credit you
in the release notes.
The encryption model is documented in resources/architecture.md. The internal-API auth model is in docs/INTERNAL_API_AUTH.md.
Contributing
Bug reports and PRs are welcome. The codebase follows Go community conventions
(gofmt, go vet), TypeScript with the web/ config, and Rust with
cargo fmt. Please run the full test suite before opening a PR.
For larger changes, open an issue first to discuss the approach. The maintainers respond fastest to PRs that:
- Stay scoped to one logical change
- Keep the worker free of new direct-data-service dependencies (no Postgres
in workers; route through
/api/v1/internal/*instead) - Add tests for new business logic
- Don't break self-hostability (any new external dependency must have an
open-source path documented in
docs/VENDOR_LOCKIN.md)
License
Licensed under the Apache License 2.0. Copyright 2026 Mindroot Ltd. See LICENSE for the full text.
Built with the boring tools that actually work in production.


