> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sender Domains and Deliverability

> After this ships, a tenant's mail comes from their own domain, and one tenant's bad list cannot damage everybody else's reputation.

## Context

Deliverability is the part of email nobody thinks about until mail stops arriving, and by then the
damage is reputational and slow to undo.

Two problems. **Shared reputation**: on a shared subdomain, one tenant with a purchased list drags
down delivery for every other tenant. **Cold domains**: a brand-new domain sending 50 000 emails on
day one goes straight to spam, and the tenant blames us.

The answers are a custom domain as a paid add-on (DEC-F4) — which is both a revenue line and
reputation isolation — and enforced warm-up, which is unpopular precisely because it works.

## Scope *(normative)*

* `messaging.sender_domains` with SPF, DKIM and DMARC verification.
* A guided setup that emits the exact DNS records to publish.
* Enforced warm-up schedule for a new domain.
* `messaging.deliverability_metrics`: bounce and complaint rates per domain.
* Automatic pause when thresholds are exceeded.
* Fallback to the shared subdomain until a custom domain verifies.

## Non-scope *(normative)*

* Buying or hosting DNS. We tell the tenant what to publish; they publish it.
* Inbox placement testing across providers — a specialist service, not something we should imitate.
* Reputation repair. If a domain is blocklisted, the remedy is the tenant's sending behaviour.

## Behaviour *(normative)*

1. The default is a **shared subdomain per tenant** (`{tenant}.softcrum.com`), working out of the
   box. A custom domain is an add-on (DEC-F4), included at enterprise.
2. Setup shows the **exact DNS records**, checks them automatically, and shows which are missing.
   "Configure SPF and DKIM" as an instruction is where tenants give up.
3. Until a custom domain verifies, sending continues on the shared subdomain. A tenant is **never
   blocked from sending** by an incomplete DNS change.
4. A newly verified domain enters **enforced warm-up**: week 1 ≤200/day, week 2 ≤1 000, week 3
   ≤5 000, week 4 normal. Transactional first, marketing last. The limit is enforced, not advised —
   an advisory warm-up is one nobody follows.
5. Warm-up **aborts** if bounce exceeds 2% or complaints exceed 0.1%, and the tenant is told which
   number tripped it.
6. Sustained threshold breaches **pause the domain's marketing sending** while transactional
   continues. Marketing is what damages reputation; receipts are what customers need.
7. Bounce and complaint rates are visible to the tenant in the console, per domain, over time. A
   tenant who cannot see their own numbers cannot fix them.
8. DMARC alignment is checked and reported; we do not require it, because many tenants cannot change
   their organizational DMARC policy quickly.

## Data *(normative)*

| Table                              | Key invariants                                                                                                                                                                                            |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messaging.sender_domains`         | `tenant_id`; `domain` unique globally (a domain belongs to one tenant); `spf_verified`, `dkim_verified`, `dmarc_status`; `warmup_stage`, `warmup_started_at`; `is_paused`, `paused_reason`; `verified_at` |
| `messaging.deliverability_metrics` | (`domain_id`, `window_start`); sent, delivered, bounced, complained; append-only; partitioned monthly                                                                                                     |

`domain` is the one uniqueness constraint in the platform that is **global rather than per tenant**
(`standards/data.md` §2b), and for a physical reason: DNS is global, and two tenants cannot both own
`acme.com`. Verification is what proves the claim.

## API *(normative)*

| Endpoint                                        | Class      | Permission                         | Budget    |
| ----------------------------------------------- | ---------- | ---------------------------------- | --------- |
| `GET/POST /v1/messaging/sender-domains`         | Management | `messaging.domains.{read\|create}` | p95 \<1 s |
| `POST /v1/messaging/sender-domains/{id}/verify` | Management | `messaging.domains.verify`         | p95 \<5 s |
| `GET /v1/messaging/deliverability`              | Management | `messaging.deliverability.read`    | p95 \<1 s |

## Events *(normative)*

None on the outbox. Verification and pause changes notify the tenant through `messaging` itself, as
`product` category messages.

## Acceptance criteria *(normative)*

1. Setup emits DNS records that verify against a real provider, and reports which are missing.
2. An unverified domain does not block sending; mail goes out on the shared subdomain.
3. Warm-up limits are enforced: exceeding the daily cap queues rather than sends.
4. Bounce above 2% during warm-up aborts it and names the number.
5. A sustained breach pauses marketing and leaves transactional flowing.
6. A domain already claimed by another tenant cannot be added.
7. Bounce and complaint rates are visible per domain over time.
8. **Negative:** no path sends from an unverified custom domain.

## Execution

Single slice, synchronous command, plus a scheduled verification poll. Runbook:
[`../../../runbooks/deliverability.md`](/runbooks/deliverability).

## Open questions

| # | Question                                                                                          | Decides | By                       |
| - | ------------------------------------------------------------------------------------------------- | ------- | ------------------------ |
| 1 | Thresholds that pause a tenant — 2% bounce and 0.1% complaints, or tighter? (PRD open question 1) | Daniel  | before approval          |
| 2 | Is the custom domain add-on per domain or per tenant?                                             | Daniel  | before commercial launch |

## Changelog

| Version | Date       | Change        | Why | Author                 |
| ------- | ---------- | ------------- | --- | ---------------------- |
| 0.1.0   | 2026-08-17 | Initial draft | —   | daniel + claude-opus-5 |

## Delivery record

*Not implemented yet.*
