Start here

Mail Platform

A self-hosted transactional email service. Customers connect their own domains through DNS, get an API key, and send application email through a REST API — with delivery tracking, bounce handling, suppression lists and webhooks.

await mail.emails.send({
  from: "hello@example.com",
  to: "customer@gmail.com",
  subject: "Welcome",
  html: "<h1>Welcome!</h1>",
});

This is not "install Postfix and send email". Whether your mail reaches an inbox depends on correct SPF, DKIM and DMARC, a PTR record that matches your EHLO name, a clean IP, opportunistic TLS, low bounce and complaint rates, and a sending reputation built over weeks. The platform automates the parts that can be automated and tells you plainly about the parts that cannot. Before deploying, confirm your VPS provider permits outbound port 25 — most block it by default and Google Cloud blocks it permanently. See docs/deployment.md.


Architecture

                         ┌──────────────────────┐
                         │      Dashboard       │
                         │   Next.js 16 / TS    │
                         └──────────┬───────────┘
                                    │  HTTPS
                                    ▼
                         ┌──────────────────────┐
                         │      Mail API        │
                         │   Fastify 5 / TS     │
                         └──────────┬───────────┘
                          ┌─────────┴─────────┐
                          ▼                   ▼
                  ┌──────────────┐    ┌──────────────┐
                  │  PostgreSQL  │    │ Redis + Bull │
                  │   (Prisma)   │    │      MQ      │
                  └──────────────┘    └──────┬───────┘
                                     ┌───────┴────────┐
                                     ▼                ▼
                              ┌────────────┐   ┌────────────┐
                              │Mail Worker │   │Mail Worker │
                              └─────┬──────┘   └─────┬──────┘
                                    └────────┬───────┘
                                             ▼  DKIM-signed message
                                    ┌──────────────────┐
                                    │     Postfix      │◀── bounce reports
                                    │    SMTP / MTA    │    (port 25)
                                    └────────┬─────────┘
                                             ▼
                                Gmail · Outlook · Yahoo · …

Three things are worth knowing about how this fits together:

The API never speaks SMTP. POST /v1/emails validates, checks the domain, checks limits, writes a row, enqueues a job and returns 202. Remote delivery takes seconds to minutes; that must never be your request's latency.

DKIM signing happens in the worker, not in the MTA. Each customer domain has its own key, encrypted at rest, decrypted in memory at signing time. The usual alternative — OpenDKIM with a key table — means plaintext private keys on the mail server's filesystem and an MTA reload every time a customer adds a domain.

Delivery confirmation comes from the MTA's own log. Postfix returns a queue ID when it accepts a message; the worker tails mail.log and matches each status=sent / bounced / deferred line back to the message. sent means the MTA took it; delivered means the recipient's server did.


Quick start

Requires Docker and Node 20+.

git clone <this repo> mail-platform
cd mail-platform

cp .env.example .env
# Generate the four secrets:
for n in DKIM_PRIVATE_KEY_ENCRYPTION_KEY JWT_SECRET WEBHOOK_SIGNING_SECRET INTERNAL_API_TOKEN; do
  printf '%s=%s\n' "$n" "$(openssl rand -hex 32)"
done
# Paste them into .env, and set POSTGRES_PASSWORD.

docker compose up -d --build
docker compose ps          # everything should report healthy

The developer path

1. Create an account       →  /register
2. Add a domain            →  /dashboard/domains
3. Publish the DNS records →  four records, generated for your domain
4. Click Verify DNS        →  SPF and DKIM must pass
5. Create an API key       →  shown once
6. Send                    →  POST /v1/emails

Local development without Docker

npm install
npm run build              # workspace packages must be built before the apps run

docker compose up -d postgres redis    # or point DATABASE_URL/REDIS_URL elsewhere
npm run db:deploy

npm run dev:api            # :4000
npm run dev:worker         # :4001
npm run dev:dashboard      # :3000

Project layout

apps/
  api/          Fastify REST API — auth, domains, keys, sending, stats
  worker/       BullMQ processors + the Postfix log tailer
  dashboard/    Next.js 16 App Router dashboard
packages/
  database/     Prisma schema, client, migrations
  shared/       env, crypto, errors, logging, DNS record generation + checks
  validation/   Zod schemas shared by the API and the dashboard
  email-sdk/    @yourservice/node — the published client
infrastructure/
  postfix/      MTA image, main.cf template, bounce pipe
  nginx/        Reverse proxy templates, TLS example
  docker/       Application Dockerfiles
  dkim/         Key handling notes (no keys — they live encrypted in the DB)
docs/           API, DNS, deployment, operations, security, testing
tests/          Unit (no infra) and integration (Postgres + Redis)

What's implemented

Accounts — registration, login, scrypt password hashing, JWT sessions, audit log, role-based authorization, per-account suspension.

Domains — add a domain, get a per-domain DKIM key pair and four generated DNS records; verify SPF, DKIM, DMARC and the return path with specific diagnostics for each failure mode (missing, duplicate SPF, key mismatch, malformed, lookup failure); periodic re-verification that demotes a domain whose records disappear.

API keys — created from the dashboard only, shown once, stored as SHA-256, revocable, with last-used tracking.

SendingPOST /v1/emails with html/text, cc/bcc, reply-to, custom headers, tags, metadata, scheduling and up to ten base64 attachments (with cid for inline images). Validation, domain verification, suppression and quota checks all happen before anything is queued.

Sender branding — per-tenant logo and footer, injected at send time. The footer is written separately for the HTML and plain-text parts, since the right markup for each differs; either can be left empty and is derived from the other. Note this is the logo inside the message; the avatar Gmail draws beside the sender name is BIMI, which needs a paid VMC.

Queuesemail-send, email-retry, bounce-processing, dns-verification, webhook-delivery. Exponential backoff, permanent-failure detection that stops retrying 5xx, and a staged slow-retry lane for a sick MTA.

Delivery tracking — eight lifecycle statuses, a per-message event timeline, per-recipient outcomes, and the raw SMTP response.

Bounces — RFC 3464 DSN and RFC 5965 ARF parsing, hard/soft/complaint classification, automatic suppression on permanent failure, soft-bounce thresholds.

Suppressions — per-tenant, enforced at send time and re-checked in the worker in case the list changed while the message was queued.

Abuse prevention — layered rate limits (IP, user, domain, API key), conservative new-account allowances, per-recipient quota accounting, sender domain verification, configurable disposable/free-mailbox controls, and abuse signals for bounce rate, complaint rate, failure rate, volume spikes and recipient repetition.

Receiving — per-domain inbound mail on a subdomain by default, so a customer's existing mailbox is never diverted. Messages are parsed, stored with their original bytes and attachments, and reported with SPF/DKIM/DMARC verdicts rather than being silently filtered.

Webhooks — six events, HMAC-SHA256 signatures with a replay window, retries with backoff, automatic disabling of dead endpoints, delivery history.

Dashboard — overview with a delivery ledger, inbox and compose, sent log and message detail, domains with a zone-file view of the records, API keys, suppressions, webhooks, deliverability checklist, branding, profile and settings.

Operations — liveness and readiness probes, Prometheus metrics, structured JSON logs with secret redaction, health checks on every container.


Documentation

The running API also publishes all of this as plain Markdown, unauthenticated — handy for integrators without a login, and for coding agents:

https://mail-api.scanmypass.com/docs        index
https://mail-api.scanmypass.com/docs/api    any document by slug
https://mail-api.scanmypass.com/llms.txt    llmstxt.org map for language models
docs/api.mdFull API reference with cURL, JavaScript, Node, Python and PHP examples.
docs/dns.mdWhat each record does, how to publish it, and every common failure.
docs/deployment.mdVPS deployment, port 25, PTR, TLS, IP warm-up, backups, scaling.
docs/operations.mdHealth checks, metrics, alerts, logs, abuse response, runbook.
docs/security.mdThreat model, secret handling, multi-tenancy, input validation.
docs/testing.mdRunning the suites and what each one covers.
packages/email-sdk/README.mdThe Node SDK.

Commands

npm run build          # all workspace packages and apps
npm run typecheck      # tsc --noEmit everywhere
npm run lint
npm test               # unit tests; integration too when TEST_DATABASE_URL is set

npm run db:migrate     # create a migration from schema changes
npm run db:deploy      # apply pending migrations
npm run db:studio      # Prisma Studio

npm run docker:up
npm run docker:logs
npm run docker:down

Configuration

Every setting is an environment variable; see .env.example for the annotated list. The ones that decide how the platform presents itself:

VariablePurpose
MAIL_DOMAINYour service's domain. Appears in Message-IDs.
SMTP_HOSTNAMEEHLO name. Must match the PTR record of the sending IP.
SPF_INCLUDE_HOSTWhat customers put in their include:. Add sending IPs here, not to customer DNS.
BOUNCE_HOSTWhere bounce reports are addressed. Needs an MX pointing at your server.
DKIM_PRIVATE_KEY_ENCRYPTION_KEYEncrypts every DKIM private key. Not recoverable — back it up separately from the database.

Licence

MIT.