Hardcoded decay thresholds (90, 180, 120 as magic numbers scattered across cron jobs, cleanup handlers, and segment queries) create three problems. First, they drift: the cron job uses 180 days while the segment query uses 90, causing inconsistent behavior that is invisible until a contact falls into the gap. Second, they cannot be tuned for list behavior without a code deployment — if you discover your audience has a 60-day engagement cycle, changing the threshold requires finding and updating every hardcoded number across multiple files. Third, they make code review harder: a reviewer seeing 120 in a WHERE clause has no context for whether that number is intentional, inherited, or stale.
Low because scattered thresholds cause drift and maintenance friction rather than immediate data corruption, but threshold inconsistency silently produces divergent list-state across different parts of the system.
Centralize all decay thresholds in a single config module, backed by environment variables so they can be adjusted without a code deploy:
// src/lib/config/decay.ts
export const DECAY_CONFIG = {
atRiskDays: parseInt(process.env.DECAY_AT_RISK_DAYS ?? '30'),
inactiveDays: parseInt(process.env.DECAY_INACTIVE_DAYS ?? '90'),
lapsedDays: parseInt(process.env.DECAY_LAPSED_DAYS ?? '180'),
reverifyAfterDays: parseInt(process.env.REVERIFY_AFTER_DAYS ?? '120'),
} as const
Import DECAY_CONFIG in every location that previously used a hardcoded number. A grep for numeric literals like 90, 120, 180 in the codebase should return zero results in list-hygiene logic after this change.
ID: data-quality-list-hygiene.data-decay.configurable-thresholds
Severity: low
What to look for: Count all locations where decay threshold values (e.g., 90, 120, 180 days) appear in the codebase. Check whether they are configurable via environment variables, admin settings, or a config file — or whether they are hardcoded magic numbers scattered throughout the codebase. Hardcoded thresholds cannot be tuned without a code deployment. Report the ratio of locations using the central config.
Pass criteria: Decay thresholds are defined in no more than 1 location (constants file, config object, or environment variable) and referenced consistently. Changing the threshold requires editing one place. Report the count of threshold references even on pass.
Fail criteria: Threshold values (like 90, 180, 120) are hardcoded as magic numbers in at least 2 places across the codebase, making them inconsistent or difficult to change.
Skip (N/A) when: The system has no decay management logic whatsoever (covered by the reverification-trigger check above).
Detail on fail: Example: "Decay threshold of 180 days appears as magic number in 4 different files — cron job, cleanup handler, segment query, and report"
Remediation: Centralize thresholds in a config module:
// src/lib/config/decay.ts
export const DECAY_CONFIG = {
atRiskDays: parseInt(process.env.DECAY_AT_RISK_DAYS ?? '30'),
inactiveDays: parseInt(process.env.DECAY_INACTIVE_DAYS ?? '90'),
lapsedDays: parseInt(process.env.DECAY_LAPSED_DAYS ?? '180'),
reverifyAfterDays: parseInt(process.env.REVERIFY_AFTER_DAYS ?? '120'),
} as const
Import this object wherever threshold logic runs, rather than embedding numeric literals.