# Prod-Readiness Audit — feat/documents-folders **Date:** 2026-05-11 **Branch:** `feat/documents-folders` (67 commits ahead of `main`; 34 from this session's documents-hub-split work + 33 from Wave 11.B) **Scope:** 17 parallel domain audits (data-structure & sales-process completeness appended at bottom) **Test posture at audit time:** 1287/1287 unit + integration pass. TypeScript clean (4 pre-existing errors: 1 stale `.next/` build artifact, 3 in a Wave 11.B-era `InMemoryBackend` test stub). ## Headline **~28 Critical, ~38 Important, ~36 Minor findings across 17 domains.** (Original 16-domain count was 23/32/30; Audit 17 added 5/6/6.) A handful of the Criticals are real bugs in this session's work that need to be fixed on this branch before merging to `main`. A few are long-standing gaps that survived multiple iterations (storage migration script, `.env.example` URL) and should be fixed independently of this branch but before any prod cutover. Several are mobile/a11y issues that were never going to be caught without a running dev server, which the implementation pass didn't have. **Recommendation:** fix the 23 Criticals before merging this branch. Triage Importants into "fix-before-prod" vs "follow-up-on-main". Minors → backlog. Estimated effort to clear Criticals: 6-10 hours of focused work. --- ## Critical findings Grouped by remediation domain. Each entry: brief rationale + file:line ref + fix sketch. ### A. Core feature regressions in this session's work **A1. `handleDocumentCompleted` is not idempotent — Documenso retries duplicate `files` rows + orphan blobs** `src/lib/services/documents.service.ts:1115` `resolveWebhookDocument` returns the doc regardless of `status`. Two webhook deliveries (Documenso retries on 5xx) can both pass through and both insert `files` rows; the second `UPDATE documents SET signedFileId` clobbers the first and the first blob is permanently orphaned in storage with no DB row. **Fix:** `if (doc.status === 'completed' && doc.signedFileId) return;` immediately after `resolveWebhookDocument`. Standard idempotency gate for this pattern. **A2. Realtime hookup dropped by hub rebuild — multi-rep stale data** `src/components/documents/hub-root-view.tsx`, `src/components/documents/entity-folder-view.tsx` The pre-rebuild hub consumed `document:*` and `file:*` Socket.IO events via `useRealtimeInvalidation`. After the rebuild, both `HubRootView` and `EntityFolderView` have no realtime subscription at all. The remaining hook lives inside `FlatFolderListing`, which is torn down when navigating away. Result: rep A on `Clients/Smith/` will not see rep B's upload until manual refresh; webhook-completed signatures don't appear in the Signing-in-progress section. **Fix:** lift `useRealtimeInvalidation` up to `DocumentsHub` with both `document:*` and `file:*` events targeting the prefix keys `['files']` and `['documents']`. TanStack Query prefix matching will invalidate the aggregated keys. **A3. LEFT JOIN port_id in ON clause defeats `idx_docs_signed_file_id`** `src/lib/services/files.ts:544` ```sql LEFT JOIN documents d ON d.signed_file_id = f.id AND d.port_id = $portId ``` Planner picks `idx_docs_port` and applies `signed_file_id = f.id` as a residual filter. At scale this is 20 × N comparisons per page load instead of 20 point lookups. Same pattern in `documents.service.ts:1915` for the workflow projection. **Fix:** drop `AND d.port_id = portId` from the ON clause and add `AND (d.port_id = portId OR d.id IS NULL)` to the outer WHERE. Or add a composite `(signed_file_id, port_id)` index. `files.port_id` is already scoped, so cross-port leak risk is zero. **A4. Importer doesn't set `files.folder_id` — imported files invisible to folder queries** `scripts/import-organized-documents.ts:196-208` The `documents` row gets `folderId` correctly (line 216) but the companion `files` row does not. `files.folder_id` is a separate column. The backfill won't rescue these — it only acts on files with entity FKs set, and the importer sets none of those either. **Fix:** copy `folderId` into the `files.values(...)` block alongside the document insert. **A5. `chk_system_folder_shape` has NULL escape — corrupted system rows persist** `src/lib/db/migrations/0051_documents_hub_split.sql:22-28` `NOT system_managed OR entity_type = 'root' OR (...)` evaluates to `NULL` (not `false`) when `entity_type IS NULL` and `system_managed = true`. Postgres treats NULL as "not false" so the constraint passes. Confirmed by direct insert test. **Fix:** add `entity_type IS NOT NULL` to the constraint, or restructure as `CHECK (NOT system_managed OR (entity_type IS NOT NULL AND (entity_type = 'root' OR (entity_type = ANY(...) AND entity_id IS NOT NULL))))`. **A6. `document-folders.service.ts` has zero log lines — silent failures across the entire folder service** `src/lib/services/document-folders.service.ts` (no `logger` import) Orphan rows in `listTree` are silently dropped (line 83-84). The 50-attempt suffix-loop exhaustion throws `ConflictError` with no log. `ensureSystemRoots` "missing root after upsert" throws raw `Error`. At 3am you would have no diagnostic for folder-related failures. **Fix:** `import { logger } from '@/lib/logger'`. Add `logger.warn` on orphan-detection, retry-exhaustion (both `ensureEntityFolder` and `syncEntityFolderName`), and the missing-root invariant in `ensureSystemRoots`. **A7. `demoteSystemFolderOnEntityDelete` is not wired into `client-hard-delete.service.ts`** `src/lib/services/document-folders.service.ts:650` (exported but zero callers) `client-hard-delete.service.ts` exists. It clears entity FKs on `files` and `documents` inside its transaction but never demotes the system folder. After hard-delete: folder retains `system_managed=true` + the dead `entity_id`. The partial unique index `uniq_document_folders_entity` permanently blocks any future client folder that would get the same display name. Also a GDPR right-to-be-forgotten gap. **Fix:** call `demoteSystemFolderOnEntityDelete(portId, 'client', clientId)` inside `hardDeleteClient`'s transaction (or as a post-commit hook with audit log). Confirm whether `companies`/`yachts` have analogous hard-delete services that also need wiring. ### B. Accessibility blockers (WCAG 2.1 AA failures) **B1. Unlabeled search input** `src/components/documents/documents-hub.tsx:265` `` — placeholder is not a label. Fails WCAG 1.3.1 / 4.1.2. **Fix:** `aria-label="Search documents by title"`. **B2. No `aria-pressed` on type-filter chips** `src/components/documents/documents-hub.tsx:276-299` Active state is purely visual. Screen readers can't tell which chip is selected. Fails WCAG 4.1.2. **Fix:** `aria-pressed={typeFilter === t}` on each chip. **B3. No `aria-expanded` on tree chevrons; folder-row labels lack context** `src/components/documents/folder-tree-sidebar.tsx:125, 135-155` The expand button has `aria-label="Collapse"` / `"Expand"` with no folder name, so SR users hear "Expand button, Expand button…" with no differentiation. And it lacks `aria-expanded` so the open/closed state is invisible. **Fix:** `aria-expanded={open}`, `aria-label={\`${open ? 'Collapse' : 'Expand'} ${node.name}\`}`. Same pattern in `documents-hub.tsx:210-217` for the per-row signer expand. **B4. `aria-label` on Lock SVG becomes part of button's accessible name** `src/components/documents/folder-tree-sidebar.tsx:150-154` `` inside the folder-select `