Surfc — Tech Debt Audit
Surfc — Tech Debt Audit
Date: 2026-04-24
Scope: React PWA in surfc/ only (marketing site surfc-web/ not audited)
Context: SUR-220 migrates app-shell routing to react-router-dom. SUR-247 is the
follow-up that replaces the useUI view-state machine with route-derived state.
This audit is sequenced so the phased plan plugs into both.
Summary
Three structural problems account for most of the debt visible from App.jsx:
- App.jsx is a junction drawer. It hosts auth/encryption gates, the full Unlock screen (135 inline lines), media-query detection, capture-animation state, action-sheet management for both notes and ideas, responsive nav construction, and all 14 view switch-cases. At 762 lines it is 25% larger than CLAUDE.md claims and continues to absorb concerns.
useUIview-state is not routing. The hand-rolled popstate bridge insrc/hooks/useUI.js+ the pathname workaround insrc/router.jsform two parallel navigation systems that coordinate through_sidtokens. SUR-220 is the correct reset; this audit flags what must land with it and what can wait for SUR-247.- Two mega-hooks own most app behaviour.
useKeyManagement.js(816 lines) anduseNoteForm.js(466 lines) each mix three or four concerns. They are the next-biggest refactor surface afterApp.jsxitself and are not blocked by SUR-220 — they can be done in parallel.
The rest is a tail of known-but-uncaptured cleanup: broken #faq links on
authenticated pages, a deprecated iOS meta tag, posthog-node leaking into
the client bundle, stale SYSTEM_ARCHITECTURE.md, and zero direct unit
coverage on db.js migrations.
App.jsx — structural walkthrough
Starting at the file the user pointed at, here is what is in it and where each concern should move.
| Lines | Concern | Where it should live |
|---|---|---|
| 36–171 | UnlockScreen component | src/components/UnlockScreen.jsx — its own file, pulls PIN-redeem UI out of the shell. |
| 178–188 | isDesktopLayout/isTabletLayout resize listener | src/hooks/useMediaQuery.js (new); also replaces the duplicated window.innerWidth >= 640 check in useUI.js:4. |
| 231–260 | captureAnimImg + capturePreviewPendingRef + handleCaptureReady/handleCapturePreviewEnd | Inside useNoteForm (owns the capture lifecycle already) or a dedicated useCaptureAnimation hook. |
| 262–279 | activeNote/activeAction/activeIdea/activeIdeaAction + handlers | useActionSheet hook or extract <NoteActionOverlay> and <IdeaActionOverlay> components. Current shape guarantees every App-level re-render touches sheet state. |
| 303–366 | Auth/waitlist/encryption/migration gate ladder | Extract <AppGates> wrapper so App renders a single authenticated tree. Also sets up a clean boundary for the SUR-220 router (gates must run before the route tree mounts). |
| 381, 422–429 | activeIdeasCount, hasMouse, canCapture, desktopTabs, bottomNavItems | Move the derived nav model into a <ShellNavigation> component. window.matchMedia(...) is called on every render today (line 422) and isn’t reactive. |
| 431–687 | The big JSX — mobile-view switch, nav bar, sidebar/detail columns | Once SUR-220 lands this becomes <Routes> and most of this file disappears. |
Specific smells to call out in the JSX block:
- Four
${ui.mobileView === '...'conditionals wrappingmain-contentdivs (473, 495, 512, 530, 543, 557, 588, 600) plus two special-caseview-visible/view-hiddenblocks (617, 630). Classic “early router” scaffolding that SUR-220 collapses. - 16
style={{}}inline-style hits in this file alone (86 across the app) — mixes CSS-variable convention with ad-hoc inline props, mostly in the UnlockScreen block. bottomNavItemsanddesktopTabsduplicate theirisActivepredicates (['home', 'sources', 'active-ideas'].includes(ui.mobileView)) — these string-array contains checks become route matches post-SUR-220.
After the extractions in the table above, App.jsx can reasonably get under
200 lines (gates + router + global overlays).
Prioritised debt list
Scored Priority = (Impact + Risk) × (6 − Effort) per the tech-debt playbook.
Impact/risk/effort are 1–5.
P0 — do alongside SUR-220
| # | Item | Category | Impact | Risk | Effort | Priority |
|---|---|---|---|---|---|---|
| 1 | Extract UnlockScreen, <AppGates>, <ShellNavigation>, action-sheet overlays from App.jsx. Add useMediaQuery hook. | Code | 5 | 3 | 3 | 24 |
| 2 | Fix SUR-209 dead #faq / #what-is-surfc links in ProfileScreen.jsx:205 and SettingsModal.jsx:95 | Infra/UX | 3 | 2 | 1 | 25 |
| 3 | Remove posthog-node from dependencies (move to dev if still needed for scripts) | Dependency | 2 | 2 | 1 | 20 |
| 4 | Replace window.confirm unsynced-changes dialog in useAuth.js:308 with a styled modal (or accept native dialog explicitly) | Code/UX | 2 | 2 | 2 | 16 |
P1 — next refactor sprint (post-SUR-220, before v1.1)
| # | Item | Category | Impact | Risk | Effort | Priority |
|---|---|---|---|---|---|---|
| 5 | SUR-247: replace useUI with route-derived state + drop previousMobileView/previousNoteView history shims | Architecture | 4 | 3 | 4 | 14 |
| 6 | Split useKeyManagement.js (816 lines → useEncryptionEnrollment, useEncryptionUnlock, useDeviceTransfer, useLegacyMigration, signInEncryptionCheck) | Code | 3 | 4 | 4 | 14 |
| 7 | Split useNoteForm.js (useCapture / useTranscribe / useIdeaDiscovery / useNoteFormState) and rework NoteForm’s 30-prop interface (context or colocated state) | Code | 4 | 3 | 3 | 21 |
| 8 | Decompose syncFromCloud (86-line function in useAuth.js:160) into explicit phases: outbox-flush, cloud-pull, book-backfill, image-hydrate, reload | Architecture | 3 | 4 | 3 | 21 |
| 9 | Add direct unit tests for db.js CRUD + every db.version(n).upgrade path (currently untested — flagged in CLAUDE.md) | Test | 2 | 4 | 3 | 18 |
| 10 | Split App.behaviour.test.jsx (480 lines) into focused specs (gates, unlock, capture, action-sheets) — paves the way for extracting App.jsx | Test | 2 | 3 | 2 | 20 |
P2 — background cleanup
| # | Item | Category | Impact | Risk | Effort | Priority |
|---|---|---|---|---|---|---|
| 11 | Refresh docs/architecture/SYSTEM_ARCHITECTURE.md (stale banner per CLAUDE.md, doesn’t describe surfc-web split) | Docs | 2 | 2 | 2 | 16 |
| 12 | Replace deprecated apple-mobile-web-app-capable meta in index.html:8 with mobile-web-app-capable (keep legacy tag for older iOS) | Infra | 1 | 2 | 1 | 15 |
| 13 | ADRs for (a) Master Key architecture, (b) two-repo split SUR-218, (c) SUR-201 pathname-router workaround. Missing structural context for new contributors. | Docs | 2 | 2 | 2 | 16 |
| 14 | Further split useAuth.js into useSession/useSync/useAccountDeletion (344 lines post–km extraction) | Code | 2 | 2 | 3 | 12 |
| 15 | Consolidate inline style={{}} (86 hits) into styles.css so theming is enforceable; audit use of CSS vars | Code | 2 | 1 | 3 | 9 |
| 16 | Remove callTranscribeImage/callDiscoverIdeas double-wrapper in src/api.js vs invokeAnthropicProxy in src/supabase.js — pick one | Code | 1 | 2 | 2 | 12 |
| 17 | Strip debugLog/addDebug path from useNoteForm.js (production-visible debug surface) | Code | 1 | 1 | 1 | 10 |
| 18 | Remove src/router.js policy-route handling once SUR-220 lands — Netlify 301-bounces these now, so the matcher is dead code | Code | 1 | 1 | 1 | 10 |
Phased plan
The plan is designed so feature work can continue in parallel. Nothing here requires a freeze.
Phase 0 — Unblock SUR-220 (1 sprint, already in-flight)
Merge with the SUR-220 PR or as a short stack of commits beneath it:
- #1 App.jsx extraction pass.
UnlockScreen,AppGates,ShellNavigation, action-sheet overlays,useMediaQuery. Target: App.jsx < 250 lines. - #2 SUR-209 link fix. One-line change in each of two components, either
cross-domain to
surfc.app/#faqor in-app help page if SUR-209 adds one. - #3
posthog-nodedependency cleanup. Ifscripts/needs it, move to devDependencies and verify the client build shrinks. - #4
window.confirm→ styled modal for the unsynced-changes path.
Justification: each of these is independent of the router choice, stops
compounding debt in App.jsx, and shrinks the surface SUR-220 has to touch.
Phase 1 — SUR-247 foundation (next sprint after SUR-220)
- #5 SUR-247. With routes in place, delete the
useUIhistory bridge, the_sidcoordination,previousMobileView, and thesrc/router.jsmatcher. ReplacemobileViewchecks withuseLocation/<Route>matches. - #10 Break up
App.behaviour.test.jsxbefore major component splits — 480-line tests are hard to move with confidence.
Phase 2 — Mega-hook decomposition (v1.1)
- #6
useKeyManagementsplit. Natural seams at the// ──comments already in the file (Reset / Eager restore / Enrollment / Unlock / Transfer / Migration / Sign-in check). KeepkeyManagermodule-level state untouched — only the React wiring is redistributed. - #7
useNoteFormsplit +NoteFormcontext. Biggest developer-velocity win in the file audit: the 30-prop component is the single loudest prop-drilling smell in the repo. - #8
syncFromCloudphasing. Makes sync failures observable (which phase failed?) and unlocks targeted retries.
Phase 3 — Supporting debt (background, any sprint)
- #9 db.js unit tests. Highest-risk untested surface — a migration bug here silently corrupts user data on device.
- #11–13 Docs + ADRs. Low-risk, high-onboarding-value.
- #14–18 Tail cleanup.
Business justification (short form)
- Velocity today is capped by
App.jsx. Every feature that touches auth/encryption/navigation lands in the same file, which makes review slow and test setup expensive. The Phase 0 extraction is a one-time cost that repays on every PR afterwards. - SUR-247 is the correct shape for v1.1’s
App.jsxrefactor. Doing the router migration first (SUR-220) then the view-state replacement (SUR-247) avoids re-doing the JSX splits twice. useKeyManagementat 816 lines is the largest file in the repo and owns the crypto critical path. A bug there is user-data-destructive. Splitting it gives us narrower tests and smaller blast radius on changes.db.jsmigrations have no direct coverage. Each new Dexieversion(n)currently ships on trust. This is the one place where the cost of a silent failure is “lose a user’s notes” rather than “show the wrong screen”.- SUR-209 and the
posthog-nodebundle inclusion are day-one quick wins — they aren’t blocking anything, but they’re embarrassing when noticed.
Quick wins (< 1 hour each)
If you need a low-risk warmup before the structural work:
- SUR-209 link fix (#2)
posthog-nodedependency cleanup (#3)- Deprecated meta tag (#12)
- Strip
debugLogfromuseNoteForm(#17) - Delete
matchPolicyRoute+ related code paths fromrouter.jsonce SUR-220 is in (#18)
Each is < 20 lines of change with existing test coverage sufficient to verify.