# Per-domain and per-IP reputation tracked separately

- **Pattern:** `ab-000931` (`deliverability-engineering.warmup-reputation.reputation-tracking-separation`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000931
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000931)

## Why it matters

Domain reputation and IP reputation are independent signals that mailbox providers track and report separately — Google Postmaster Tools explicitly distinguishes them. A domain reputation hit means your content or authentication history is the problem; an IP reputation hit means the specific IP's sending behavior is the problem. If you only track a combined metric, you cannot determine whether to reconfigure content and authentication (domain issue) or rotate to a new IP (IP issue). Without separation, diagnosing and recovering from deliverability problems is guesswork that delays resolution and extends inbox-blocking.

## Severity rationale

Low because conflating domain and IP reputation metrics slows diagnosis and recovery time rather than causing immediate inbox failure, but the operational cost of undiagnosed reputation problems compounds over time.

## Remediation

Store domain and IP reputation as separate Prisma models, each with its own `updatedAt` and queryable by its identifier. Use this schema or equivalent in `prisma/schema.prisma`:

```ts
model DomainReputation {
  id           String   @id @default(uuid())
  domain       String   @unique
  spamRate     Float?
  inboxRate    Float?
  dkimPassRate Float?
  spfPassRate  Float?
  dmarcPassRate Float?
  updatedAt    DateTime @updatedAt
}

model IpReputation {
  id              String   @id @default(uuid())
  ipAddress       String   @unique
  spamRate        Float?
  microsoftCategory String? // Green / Yellow / Red
  lastChecked     DateTime
  updatedAt       DateTime @updatedAt
}
```

Populate `DomainReputation` from Google Postmaster Tools and `IpReputation` from Microsoft SNDS on a daily cron. Alerting logic should query each table independently so a domain reputation alert does not suppress an IP reputation alert.

## Detection

- **ID:** `reputation-tracking-separation`
- **Severity:** `low`
- **What to look for:** Count all reputation metric storage tables or objects. Check whether the system distinguishes between domain reputation and IP reputation as separate metrics. Look for database tables or objects that store reputation scores, complaint rates, or bounce rates indexed by both domain and IP address independently. Check if the code processes Google Postmaster Tools data (which reports domain reputation separately from IP reputation) and maps each to the correct entity.
- **Pass criteria:** At least 2 separate storage entities exist for reputation metrics: one indexed by domain and one indexed by IP. The system can report the reputation status of a specific domain independent of which IP is currently sending for it.
- **Fail criteria:** Reputation is tracked as a single combined metric with no separation of domain vs. IP, or reputation tracking does not exist at all.
- **Skip (N/A) when:** The project has a single sending domain and single dedicated IP where conflation of domain and IP reputation is acceptable.
- **Detail on fail:** `"Reputation tracked as a single metric with no domain/IP separation — cannot identify whether a reputation issue originates from the domain's DMARC/content history or from a specific IP's behavior"` or `"No reputation tracking at all — no historical signal to guide sending decisions"`
- **Remediation:** Store reputation signals indexed by both domain and IP:

  ```ts
  // Database schema (Prisma example)
  model DomainReputation {
    id              String   @id @default(uuid())
    domain          String   @unique
    spamRate        Float?   // Google Postmaster Tools domain reputation
    inboxRate       Float?   // % landing in inbox (from seed list or Postmaster)
    dkimPassRate    Float?
    spfPassRate     Float?
    dmarcPassRate   Float?
    updatedAt       DateTime @updatedAt
  }

  model IpReputation {
    id           String   @id @default(uuid())
    ipAddress    String   @unique
    spamRate     Float?   // Google Postmaster Tools IP reputation
    microsoftSndsCategory String?  // Green/Yellow/Red
    lastChecked  DateTime
    updatedAt    DateTime @updatedAt
  }
  ```

## External references

- iso-25010 reliability

Taxons: observability

HTML version: https://auditbuffet.com/patterns/ab-000931
