0014 — Dashboard stats library + KPI snapshot Lambda

Renamed KPI → Metric on 2026-04-30 (PR-followup to PR #525). This ADR's body preserves the original wording from when KPI was the canonical term. Code, data, AWS resources, and active docs all use "metric" now; this file is kept verbatim as historical record. See docs/planning/kpi-to-metric-rename.md.

Context

ADR-0012 carved out a narrow exception for Vercel Cron: the KPI snapshot job lived in the Next.js app so it could reuse the ~700 lines of computation inside /api/dashboard/stats/route.ts without duplication or HTTP self-call from a Lambda. The tradeoff was "one more place to check when listing scheduled jobs."

A follow-up (tracked as A.3 in docs/planning/dashboard-stats-extraction.md) did the refactor that made the carve-out unnecessary: the computation moved into a standalone library that any caller — HTTP route, cron, Lambda, future reports — can import directly.

With the library in place, there is no longer a reason for the cron to live on Vercel. The job moves to Lambda like every other scheduled job.

Decision

Three linked decisions, shipped together:

Location Consumed by
Dashboard stats library src/lib/dashboard-stats/ HTTP route, KPI Lambda, tests, future reports/exports
Report-data library src/lib/dashboard-stats/report-data.ts HTTP route, KPI Lambda
KPI snapshot Lambda lambda/kpi-snapshot/ EventBridge cron(0 4 * * ? *)

ADR-0003 applies in full again. Vercel Cron is not used. Every scheduled job runs on Lambda + EventBridge.

Consequences

Easier

Harder

Follow-up

Alternatives considered

Keep Vercel Cron for KPI snapshot (ADR-0012's position). Rejected once A.3 landed — the only argument for it was "don't duplicate the computation into a Lambda bundle." With the library, the bundle is trivial (~30 lines of handler), so the argument disappears.

Lambda self-calls HTTP to the Vercel endpoint. Rejected. Same pattern ADR-0003 rejected for listings-sync: adds a Lambda that does nothing but trigger Vercel.

Skip the library extraction and leave stats computation inline. Rejected per A.3 — multiple future callers (reports, exports, Clara context, mobile API) will want direct access to the same computation. Extracting once amortizes across all of them.

File index (implementation)