# Automated staleness warnings sent after 90 days without verification

- **Pattern:** `ab-001033` (`directory-listing-schema.data-freshness.staleness-warnings`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001033
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001033)

## Why it matters

Stale directory listings are the primary user trust failure for directories: a restaurant that closed 18 months ago still showing 4.5 stars with no freshness signal sends users to a shuttered door. Schema-org `dateModified` enables recency scoring by search engines and AI crawlers — but only if the field reflects actual verification, not the creation date. Without automated staleness detection and owner alerts at 90 days, your directory degrades passively: every listing drifts toward inaccuracy while appearing current. Data-integrity and observability taxons both apply here — the system must monitor its own data freshness, not rely on owners to self-report changes.

## Severity rationale

High because undetected stale listings cause real-world user harm (visiting closed businesses, calling disconnected numbers) and erode the directory's core trust proposition over time.

## Remediation

Add a scheduled route (or Vercel cron job) that queries listings with `dateVerified` older than 90 days and sends owner alerts. Run it daily.

```ts
// src/app/api/cron/stale-listings/route.ts
export async function GET(request: Request) {
  // Verify cron secret
  if (request.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 });
  }

  const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
  const stale = await db.listing.findMany({
    where: { dateVerified: { lt: ninetyDaysAgo }, status: 'active' },
    select: { id: true, ownerEmail: true, name: true },
  });

  for (const listing of stale) {
    await sendVerificationReminderEmail(listing.ownerEmail, listing.name);
  }

  return Response.json({ processed: stale.length });
}
```

Register this route in `vercel.json` under `crons` with a daily schedule (`"0 9 * * *"`). Display a staleness badge on the listing UI when `dateVerified` is more than 90 days old.

## Detection

- **ID:** `staleness-warnings`
- **Severity:** `high`
- **What to look for:** Enumerate all listings and their last-verified dates. For each, check the codebase or database for a background job, cron task, or webhook that checks listing `dateModified` or `dateVerified`. Look for logic that sends warnings when a listing hasn't been verified in 90+ days.
- **Pass criteria:** A background process checks for stale listings (no verification for over 90 days) and sends automated warnings to the listing owner — listings not verified within 90 days must display a staleness warning to users. Report even on pass: "X listings checked, Y flagged as potentially stale."
- **Fail criteria:** No staleness checking exists, or warnings are sent manually.
- **Skip (N/A) when:** There's no concept of listing verification or owner management.
- **Cross-reference:** The `verification-timestamp` check ensures the timestamp field exists for staleness calculation.
- **Detail on fail:** Example: `"No background job exists to detect stale listings"` or `"Staleness threshold is 180 days instead of 90"`
- **Remediation:** Add a scheduled job to check and alert for stale listings:

  ```ts
  export async function POST(request) {
    const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)

    const staleListing = await db.listing.findMany({
      where: { dateVerified: { lt: ninetyDaysAgo } }
    })

    for (const listing of staleListing) {
      await sendEmail(listing.ownerEmail, {
        subject: 'Your listing verification is expiring'
      })
    }

    return NextResponse.json({ processed: staleListing.length })
  }
  ```

## External references

- schema-org dateModified

Taxons: data-integrity, observability

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