Technical Debt

Known debt items with context and remediation plans

Not all debt is bad — some is deliberate trade-offs. This register tracks both.

P0 — Production Blockers

2026-04-28 — FILE DOES NOT EXIST

src/mocks/validation.ts was removed. Production uses enrichV7() with real lead scoring (lead_score >= 5). The 4 layer fields in leads table exist for backward compatibility but don’t drive validation. Real validation layer files (src/validation/layers/) exist but are unimported.

TD-001: Legacy 4-Layer Validation Fields

Files: src/validationEngine.ts, src/validation/layers/* Debt: 4-layer validation fields (layer_registry, layer_website, layer_profiles, layer_news) populated for backward compatibility but don’t drive is_validated decision. Actual validation uses lead_score >= 5. Why it exists: Transition from v4 validation engine to v7 enrichment pipeline. Legacy fields kept for API compatibility. Impact: Low — fields are accurate but unused for validation logic. Remediation: Either wire up real layer checks or deprecate the fields. Effort: 1-2 days Owner: Unassigned

TD-002: Opt-Out Never Checked in Production

File: src/scraping/middleware.ts (definition only) Debt: isOptedOut() exists but is never called in any API endpoint or worker. Why it exists: Implemented as middleware but not integrated into request pipeline. Impact: Critical — GDPR violation. Opted-out individuals still processed. Remediation: Add middleware-level opt-out gate on ALL data access endpoints. Effort: 1-2 days Owner: Unassigned

TD-003: Article 14 Notifications Never Sent

File: src/lib/article14Notification.ts:259-265 Debt: SMTP not configured. All notifications logged as “skipped”. Why it exists: Email infrastructure not set up. Relies on EMAIL_API_URL env var. Impact: Critical — Bisnode-level GDPR violation risk. Remediation: Configure SendGrid/Mailgun/Postmark. Add fallback delivery mechanism. Effort: 2-3 days Owner: Unassigned

P1 — High Priority

TD-004: Playwright Inline in Workers

File: src/queues/workers.ts, src/extractors/websiteExtractor.ts Debt: Playwright runs synchronously inside BullMQ worker threads. Why it exists: Simplest implementation. Decoupling adds complexity. Impact: High — OOM/deadlock at >40 concurrent jobs. Scaling bottleneck. Remediation: Extract Playwright to dedicated microservice with its own queue. Effort: 1-2 weeks Owner: Unassigned

TD-005: No Automated Data Retention

Files: All PII tables Debt: No retention_expires_at columns. No cleanup jobs. Why it exists: Focus on enrichment pipeline. Compliance deferred. Impact: High — GDPR Art. 5(1)(e) storage limitation violation. Remediation: Add retention fields + scheduled cleanup jobs. KB recommends 12-24 months for contact data. Effort: 3-5 days Owner: Unassigned

TD-006: pg.Pool and ioredis Still Used

Files: src/db/index.ts, src/lib/redis.ts, src/cache.ts, src/api/middleware/rate-limit.ts Debt: Project convention requires Bun.sql and Bun.redis, but legacy connections still exist. Multiple pool configurations (6+ pools with same credentials). Why it exists: Migration incomplete. Some code paths depend on pg.Pool features. Impact: Medium — inconsistent connection handling, multiple pools, no central connection manager. Remediation: Migrate all database access to Bun.sql. Migrate Redis to Bun.redis. Create central connection manager. Effort: 1-2 weeks Owner: Unassigned

TD-007: SCB Data is Synthesized

File: src/fetchers/scb/index.ts Debt: SCB API returns statistical aggregates, not real companies. orgNr synthesized from dimension codes. Why it exists: SCB doesn’t expose real company registry. Only statistics. Impact: Medium — data quality issue for SCB-derived records. Remediation: Use Bolagsverket as primary source. SCB only for advertising_block flag. Effort: 2-3 days Owner: Unassigned

P2 — Medium Priority

TD-008: No Circuit Breaker on External APIs

Files: src/enrichment/sources/*.ts Debt: External API failures (429s, timeouts) kill entire enrichment job. Why it exists: Error handling added per-source but no global circuit breaker. Impact: Medium — single API failure wastes entire enrichment effort. Remediation: api_circuit_state table exists but not actively used. Wire up circuit breaker logic. Effort: 3-5 days Owner: Unassigned

TD-009: Redis Cache Stores PII Unencrypted

File: src/cache.ts Debt: Enrichment cache (Redis DB 1) stores full contact data including names, emails, phones. Why it exists: Performance — encryption would add latency. Impact: Medium — if Redis compromised, PII exposed. Remediation: Encrypt sensitive fields in cache. Or use Redis ACL to restrict access. Effort: 2-3 days Owner: Unassigned

TD-010: HASH_SALT Module-Level Constant

File: src/compliance.ts:14 Debt: HASH_SALT captured at import time. Cannot rotate without restart. Why it exists: Simplest implementation. Impact: Low-Medium — salt rotation requires full restart. Remediation: Read salt at runtime per-hash. Add salt versioning. Effort: 1 day Owner: Unassigned

TD-011: No RoPA Logging on SELECT

Files: All GET endpoints Debt: RoPA only logs writes (INSERT/UPDATE/DELETE). No audit trail for data access. Why it exists: Read logging would be extremely verbose. Impact: Medium — cannot demonstrate who accessed what data. Remediation: Add optional read logging for sensitive endpoints. Or log at API gateway level. Effort: 3-5 days Owner: Unassigned

TD-012: Kundkort CSV Export Missing Reklamspärr Filter

File: src/api/kundkort.ts:962-1085 Debt: exportCsv() does not call isScbAdvertisingBlocked(). Why it exists: Oversight during export implementation. Impact: Medium — blocked companies can be exported. Remediation: Add reklamspärr check to export function. Effort: 2 hours Owner: Unassigned

Deliberate Debt (Won’t Fix)

TD-W01: No Real-Time Notification Delivery

Context: Art. 14 notifications are queued, not real-time. Rationale: Batch delivery is acceptable under Art. 14(3)(a) “within one month”. Real-time would overload SMTP.

TD-W02: FPR 21.4% in Production

Context: Best experiment achieved 2.6% FPR, production config at 21.4%. Rationale: Production includes timeouts and parked domains in test set. Fixable by batch additions to INVALID_NAME_STANDALONE_WORDS.

TD-W03: No Nordic Expansion Yet

Context: Norway/Denmark registries not integrated. Rationale: Swedish market not yet saturated. Expansion after product-market fit.

TD-W04: Firecrawl Phase 2 Not Done

Context: A/B comparison between Crawlee and Firecrawl incomplete. Rationale: Crawlee meets current quality bar. Firecrawl reserved for future quality push.