Platform email self-hosted (Go + React + MariaDB) yang menggabungkan mail server multi-domain ala cPanel dan Sending API ala Amazon SES/Postmark dalam satu binary cvrelay. Lisensi MIT.
Status: v0.1, irisan pertama MVP: Fondasi + Sending API. Mail server (SMTP inbound :25, IMAP/POP3, Sieve) belum ada; lihat Roadmap.
| Area | Isi |
|---|---|
| Multi-tenant | Tenant, kuota harian, pause, isolasi data per tenant_id di setiap query (dites) |
| API key | cv_live_ / cv_test_, scope, batasan stream dan IP, disimpan sebagai hash |
| Domain | DNS wizard (DKIM RSA-2048 + Ed25519, SPF, return-path, DMARC, MX), verifikasi DNS, rotasi DKIM dengan overlap 48 jam |
| Sending API | POST /v1/messages, batch 500, template, lampiran, scheduled_at + batal, Idempotency-Key 24 jam, mode test |
| Antrean & MTA | Antrean per Recipient di MariaDB (FOR UPDATE SKIP LOCKED + lease), prioritas transactional, MX lookup + fallback A, null MX, TLS oportunistik, DKIM dual signing, VERP return-path |
| Retry & bounce | 1m, 5m, 15m, 30m, 1j, 2j, 4j, lalu tiap 4 jam sampai 5 hari; klasifikasi hard/soft/block/mailbox_full/policy; hard bounce masuk suppression |
| Suppression | Per tenant + global, manual, hard bounce, one-click unsubscribe (RFC 8058) untuk stream broadcast |
| Event & webhook | queued, sent, deferred, bounced, rejected, unsubscribed; webhook HMAC-SHA256, retry sampai 24 jam, dead-letter, replay, proteksi SSRF |
| Panel | React console (embed di binary) untuk tim Codeverta: tenant, domain & DNS, aktivitas + timeline, template + preview, API key, webhook, suppression |
cp .env.example .env
docker compose run --rm cvrelay ctl genkey # isi ke CVRELAY_MASTER_KEY
# isi juga CVRELAY_ADMIN_TOKEN, password MariaDB, domain platform
docker compose up -d
open http://localhost:8080 # login dengan CVRELAY_ADMIN_TOKENMembuat tenant dan API key lewat CLI:
docker compose exec cvrelay cvrelay ctl tenant create "Toko ABC"
docker compose exec cvrelay cvrelay ctl apikey create tnt_01J... --scopes=send,read_logscurl https://relay.codeverta.com/v1/messages \
-H "Authorization: Bearer cv_live_..." \
-H "Idempotency-Key: order-12345-receipt" \
-H "Content-Type: application/json" \
-d '{
"from": "Toko ABC <no-reply@tokoabc.id>",
"to": ["budi@example.com"],
"subject": "Invoice #12345",
"html": "<p>Halo Budi, total Rp 250.000</p>",
"tags": ["invoice"],
"metadata": {"order_id": "12345"}
}'
# 202 {"id":"msg_01J...","status":"queued"}Spesifikasi lengkap: api/openapi.yaml.
Setiap POST membawa X-CV-Timestamp (unix) dan X-CV-Signature: v1=<hex>, di mana hex = HMAC-SHA256(secret, "<timestamp>.<body>"). Tolak jika timestamp lebih dari 5 menit.
import crypto from "node:crypto";
const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));Kebutuhan: Go 1.24+, Node 22+, MariaDB 10.6+ (11.4 disarankan).
# MariaDB lokal untuk tes integrasi: user butuh hak CREATE/DROP DATABASE
mysql -uroot -e "CREATE USER 'cvrelay'@'%' IDENTIFIED BY 'cvrelay'; GRANT ALL ON *.* TO 'cvrelay'@'%';"
make test # go test -race ./... dengan MariaDB sungguhan
make build # build React lalu binary bin/cvrelay
cd web && npm run dev # UI dev server, /v1 diproksi ke :8080Tes integrasi membuat database sekali pakai per tes (internal/testkit); tanpa CVRELAY_TEST_DSN tes tersebut dilewati.
Dibangun dengan skill mattpocock/skills:
- domain-modeling: glosarium di
CONTEXT.mddan keputusan didocs/adr/. Pakai istilah di glosarium (Tenant, Recipient, Suppression, ...) di kode, tes, dan diskusi. - tdd: tes hanya di empat seam yang disepakati, lewat interface publik, bukan internals:
- REST API v1 (
internal/api/*_test.go): httptest ke router asli + MariaDB asli. - Delivery worker (
internal/delivery/delivery_test.go): worker mengirim ke MX palsu (go-smtpin-process), DKIM diverifikasi independen. - Modul murni:
retry,bounce,webhooksigner,dkim,mime,secret. - Webhook dispatcher (
internal/webhook/dispatcher_test.go): penerima httptest memverifikasi signature.
- REST API v1 (
cmd/cvrelay/ binary: serve, migrate, ctl
internal/
app/ composition root (merakit semua layanan)
api/ REST API v1 + tes seam API
sending/ menerima permintaan kirim, log pengiriman
delivery/ MTA keluar: klaim antrean, DKIM, SMTP, retry, bounce, unsubscribe
webhook/ signer, CRUD webhook, dispatcher
domain/ tenant/ template/ suppression/ events/
dkim/ mime/ bounce/ retry/ secret/ blob/ dnsx/ store/ id/
testkit/ DB sekali pakai, DNS palsu, MX palsu, jam palsu
migrations/ skema MariaDB (di-embed)
web/ React console (Vite + TanStack Query + Tailwind)
api/openapi.yaml
docs/adr/
- MariaDB, bukan PostgreSQL (ADR 0001): tanpa RLS, jadi isolasi tenant ditegakkan di repository dan dites.
- Satu SPA untuk admin + tenant console (ADR 0004).
- Router
chiditulis tangan, belum di-generate dari OpenAPI; migrasi memakai runner kecil sendiri (goose terbaru butuh Go 1.26). - Event
deliveredbelum dikirim terpisah; untuk SMTP keluarsentsudah berarti diterima MX tujuan.
Irisan berikut, kira-kira urut prioritas:
- SMTP relay (submission 587/465, user
apikey) untuk Laravel/WordPress/Nodemailer. - Pemroses bounce async (DSN via VERP) dan complaint (ARF/FBL), auto-pause tenant pada bounce > 5% / complaint > 0,1%.
- Integrasi API Cloudflare untuk membuat record DNS otomatis + cek DNS terjadwal tiap 10 menit.
- Throttling adaptif per provider, warmup IP, relay fallback (smart host), monitoring blocklist.
- Mail server: SMTP inbound :25, mailbox store, IMAP/POP3 (
go-imap/v2), ManageSieve, import IMAP. - Observability:
/metricsPrometheus, OpenTelemetry, ACME/TLS bawaan.