🧹 Technical Debt & Maintainability

πŸ“‹ Summary

Midshift carries a moderate, mostly well-understood level of technical debt. Roughly a third of the application's source files are touched by at least one measurable debt signal (high complexity, duplication, dead code, or an unmaintained library), but most of that is light: duplication overall is low (2.3%) and the typical function is simple. The debt that matters is concentrated in a few places: a critically out-of-date framework version, a handful of very large request handlers that mix many rules together, permission and write logic that is not applied in one consistent place, hand-run database changes, and a large test double that has to be kept in step with every server route. Unusually, the code contains no "TODO" or "FIXME" markers at all; the debt is instead recorded in prose in the project's own notes (CLAUDE.md), which is thorough but easy for a new team to miss. A new team could change this code with reasonable safety in the pure business-logic layer, which is well tested, and with considerably less safety in the server routes, which have almost no automated unit tests.

🎯 Rating

🟑 ●●●○○ 3 / 5 β€” Amber. Debt is moderate and largely documented, but the riskiest parts (authorisation, fat route handlers, manual migrations, vulnerable framework pin) sit exactly where test protection is thinnest.

πŸ” Findings

1. Debt inventory

#Debt itemCategoryEvidenceTag
D1Next.js exact-pinned at 16.2.0 with critical advisories; no automated update mechanismOutdated dependencyModule 8 Β§A2[Observed]
D2xlsx 0.18.5 frozen on npm since 2022, parses uploads server-sideUnmaintained dependencyModule 8 Β§A1; src/app/api/campaigns/[id]/products/route.ts:60[Observed]
D3Unused date-fns, google-auth-libraryDead dependencyModule 2 Β§1[Observed]
D4Fat route handlers: ad-unit PATCH (CCN 66), brief PATCH (64), review-file PATCH (59), render-push POST (53), jobs POST (52), purge cron (44) β€” validation, authorisation, persistence and side effects in one functionStructuralModule 2 Β§5[Observed]
D5forYouItems (CCN 109) β€” the "what needs me" feed in one functionStructuralsrc/lib/for-you.ts:94[Observed]
D6No data-access layer; Supabase queried directly from 154–168 filesCouplingModules 2 Β§7, 13[Observed]
D7The documented "writes go through API routes" rule is not universal: browser components write to Postgres directly (e.g. utm-generator.tsx, ad-units-grid.tsx, new-campaign modals), bypassing the server-side discipline gate and approval-reset logicInconsistent patternModule 3[Observed by Module 3]
D8Authorisation assembled per route from helpers in src/lib/api-auth.ts (3.6% unit-test coverage); the QA store re-implements the same rules (src/app/qa/store.ts:761–822)Duplication of critical logicModules 4, 7[Observed]
D9No database transactions β€” one .rpc() call in the codebase; multi-row changes rely on hand-written undo; last-write-wins on concurrent editsData integrityModule 3[Observed by Module 3]
D10Unbounded queries silently capped at Supabase's 1,000-row default (for-you.ts, purge cron archived branch)Latent defectModule 3[Observed by Module 3]
D1192 hand-applied migrations; per-deployment state recorded only in prose; 088 and 091 written but not run; README/NEW_DEPLOYMENT give out-of-date baseline instructions (baseline is 001–090 + 092, docs say 001–061)Operationalsupabase/baseline.sql:2; README.md:70–71; Module 12[Observed]
D12QA harness (β‰ˆ4,500 lines; store.ts 3,349 lines, CCN 1,074 summed) must mirror all 125 route handlers; enforced by scripts/harness-contract.jsMaintenance taxModule 2 Β§7[Observed]
D13Dead code acknowledged but not removed: src/app/(app)/campaign/[id]/checklist/, build/, e2e/checklist.spec.tsDead codels confirms present; CLAUDE.md "Parked decisions" says "to be deleted"[Observed]
D14Orphaned schema kept on purpose: studio_crits, studio_crit_comments, campaign_members.is_creative_approver, the brief/review pipeline tables, legacy account_manager in RLS policiesDead schemaCLAUDE.md "Migrations", "Parked decisions"; supabase/baseline.sql:46–50[Observed]
D15Role vocabulary drift: app accepts global role reviewer (src/types/database.ts:3, src/app/api/admin/invite/route.ts:7) but the user_role enum lacks it (supabase/baseline.sql:46–50)Latent defectModule 1[Observed]
D16"Bridge" code awaiting a migration: ratios parsed from format names until 088 runs (src/lib/artwork-aspect.ts)Temporary workaroundCLAUDE.md "088"[Observed]
D17Long-lived one-year signed storage URLs persisted in DB columns; cannot be revoked and will expireDesign debtModule 13[Observed by Module 13]
D18Process documentation tailored to AI coding sessions and an external claude.ai "board"Knowledge debtModules 10, 12[Observed]
D19CI uses npm ci --legacy-peer-deps as a flagged workaroundBuild.github/workflows/ci.yml[Observed]

2. Marker count

grep -rn for TODO, FIXME, HACK, XXX, @deprecated across src, supabase, scripts, e2e: 0 occurrences of each. [Observed] This does not mean there is no debt; it means debt is tracked in CLAUDE.md and docs/HANDOVER.md ("The queue, in order", "Parked decisions") rather than in code. A new team should migrate those into an issue tracker. [Inferred]

3. Maintainability β€” how safely could a new team change this?

FactorAssessmentBasis
Test protection β€” domain logicGoodsrc/lib 67% line coverage; parser 85%, asset matcher 98%, UTM 100% (Module 7)
Test protection β€” server routesPoorsrc/app/api 0.9% unit coverage; protected only by the journey suite against a mock store, not against real Postgres/RLS (Module 7)
Test protection β€” UI wiringGood, but local-only~780 journey tests (467 of 483 run here passed), not in CI
ModularityAdequateClean src/lib pure functions; fat routes and direct DB access elsewhere
DocumentationUnusually rich but partly staleModule 12: 9 of 16 sampled claims drifted
Type safety / lintGoodtsc and ESLint clean
Custom guard railsStrongwrite-results.test.ts, schema-columns.test.ts, harness contract, scrub
HotspotsUnknownNo churn data (squashed history)

Verdict: a competent team could take this over, provided it first adds tests around authorisation and the high-CCN route handlers before refactoring them. [Inferred]

4. Debt quantification

Method. Of the 395 non-test TypeScript files under src/, count a file as materially affected if it has at least one of: a function with CCN > 20 (lizard), participation in a clone β‰₯ 70 tokens (jscpd), being acknowledged dead code, or being the consumer of an unmaintained dependency.

SignalFiles
Contains a function with CCN > 2043
Participates in a duplicated block (jscpd)108 (incl. test and harness files)
Acknowledged dead code4
Uses an unmaintained dependency (xlsx)1
Union, non-test files131 of 395 β‰ˆ 33%

Estimate: about one third of source files are touched by measurable debt, but the severity-weighted share is much lower. Most duplication hits are small (2.3% of lines overall) and many sit in the QA harness mirroring production pages (e.g. workspace/campaign/[id]/layout.tsx ↔ qa/_lib/campaign-shell.tsx, 78 lines). The materially risky debt is concentrated in roughly 15–20 files: the six fat route handlers, for-you.ts, api-auth.ts, the browser components that write directly, and the QA store. [Inferred β€” judgement, method as above]

Worst duplicated pairs (jscpd, lines): workspace/campaign/[id]/layout.tsx ↔ qa/_lib/campaign-shell.tsx 78; media-strategy/route.ts ↔ wrapup/route.ts 46; review/files/route.ts ↔ upload/[token]/register/route.ts 46; workspace/campaigns/page.tsx ↔ qa/campaigns/page.tsx 44; plan/route.ts ↔ qa/store.ts 40. [Observed]

5. Prioritised remediation backlog

#ItemSeverityEffortBlocks / risks if left
1Upgrade Next.js β‰₯ 16.4; move to caret ranges + RenovateπŸ”΄ CriticalSRCE / auth-bypass exposure (Modules 8, 9)
2Fix in-job privilege escalation and approval-reset gaps found in Modules 4 and 9🟠 HighS–MSign-off integrity β€” the product's core promise
3Route all browser-side writes through API routes, or prove RLS enforces the same discipline rules🟠 HighMBypass of discipline gate and approval reset
4Unit-test api-auth.ts and the six fat handlers, then extract validation / authorisation / persistence🟠 HighMSafe change of the most complex code
5Replace xlsx🟠 HighS–MPrototype pollution / ReDoS on uploads
6Automate migrations and record per-deployment schema state; correct baseline instructions🟠 HighMSilent schema drift across client deployments
7Introduce Postgres functions (RPC) for multi-row writes needing atomicity🟑 MediumMPartial writes under failure
8Paginate or bound the 1,000-row-capped queries🟑 MediumSSilent truncation as clients grow
9Reconcile reviewer role with the DB enum🟑 MediumSAdmin action fails at runtime
10Delete dead pages/specs; plan a migration dropping orphaned Studio tables and account_manager referencesβšͺ LowSConfusion, attack surface
11Replace persisted one-year signed URLs with on-demand signing🟑 MediumMUnrevocable links; breakage after expiry
12Move the debt register from CLAUDE.md into an issue tracker; rewrite handover docs for humansβšͺ LowSKnowledge loss
13Remove unused dependencies and the --legacy-peer-deps flagβšͺ LowSSupply-chain surface

🧾 Evidence

  • grep -rn "TODO|FIXME|HACK|XXX|@deprecated" β†’ 0 each.
  • node /tmp/…/debt.js over lizard.csv and jscpd-report.json β†’ 43 complex files, 108 duplicated-block files, union 131 of 395 non-test files.
  • find src -name "*.ts" -o -name "*.tsx" | grep -v .test. | wc -l β†’ 395.
  • ls "src/app/(app)/campaign/[id]/" β†’ checklist/, build/ present; ls e2e/checklist.spec.ts present.
  • supabase/baseline.sql lines 2 and 46–50; README.md lines 70–71; src/types/database.ts:3.
  • Cross-references: Modules 2 (complexity), 3 (architecture), 4 (business rules), 5 (duplication), 7 (coverage), 8 (dependencies), 12 (documentation).

⚠️ Risks

RiskSeverityEffort
Most complex, least-tested code is the authorisation and write path🟠 HighM
Inconsistent write path (server vs browser) undermines the business rules the product sells🟠 HighM
Manual migrations across many single-tenant deployments🟠 HighM
Debt register lives in prose, not a trackerβšͺ LowS

βœ… Recommendations

RecommendationPriorityEffort
Execute backlog items 1–2 before or immediately after closeP0S–M
Execute items 3–6 in the first 60 daysP1M
Execute items 7–11 within 90 daysP2M
Items 12–13 as housekeepingP3S