Skip to content

RAID Report — 2026-05-02

Date: 02-05-2026 Subject: RAID Report

Source: docs/architecture/RISKS_GAPS_ASSUMPTIONS.md (working tree on main). Note: the sandbox could not run git log against the local checkout (no commit history reachable from the runner), so the source was read directly from the working file rather than via git show main:… as in prior runs. The file’s own change-summary banner records two new dated updates since the 01-05-2026 report — 2026-05-01 (SUR-242) adding three Azure Content Safety entries plus a calibration assumption, and 2026-05-01 (SUR-256) adding the blog rebuild gap. Previous report: RAID_REPORT_2026-05-01.md (1 day ago).

Issue NumberIssue TypeStatusDays OpenSuggested Action
1. Monolithic UI/control plane (src/App.jsx ~500 lines, owns auth gates, layout, route tree, prop fan-out)RiskOpenUnknown (predates 2026-04-23)Begin v1.1 refactor — split into focused screen components + a dedicated routing module; lift modal/overlay refs into a context.
2. Oversized hooks acting as service layers (useNoteForm ~470 lines, useSettings)RiskOpenUnknown (predates 2026-04-23)Decompose into UI-state hook + pure persistence module + ingest/PostHog dispatcher; add unit tests around the seams.
3. Schema probe only runs once after first failure [SUR-61]RiskClosed (resolved per 2026-04-26 audit — useAuth.js:175 only sets the flag after success)n/aNone — verify regression coverage in next sync test pass.
4. Manual Supabase migration application — drift detection automated but apply still manual via SQL editor [SUR-60]DependencyOpenUnknown (predates 2026-04-23)Add a CI gate that fails the build if scripts/check-schema.js reports drift on main; longer-term, adopt Supabase CLI db push from CI.
5. Managed proxy env fragility — missing SUPABASE_SERVICE_ROLE_KEY / ANTHROPIC_API_KEY surfaces as generic 500s with no operator detail in the user toastDependencyOpenUnknown (predates 2026-04-23)Add a startup self-check Edge Function that pings each required env at deploy; route 500s to a structured error code surfaced in the toast for ops.
6. Direct Anthropic exposure (BYOK) — plaintext + key from compromised browsers [SUR-63 / SUR-91]RiskClosed (SUR-91 fully sunset; only a one-time apiKey meta cleanup remains in db.js:156)n/aNone — schedule removal of the cleanup shim once telemetry confirms no remaining BYOK rows.
7. No per-note image cleanup — note-images bucket retains orphans after note delete (per-account sweep only on full account deletion)GapOpenUnknown (predates 2026-04-23)Mirror deleteCloudData’s sweep into cloudWrite for soft-deleted notes; add a nightly orphan-blob job comparing notes.imagePath to the bucket.
8. Full-table sync only — fetchSince helper unused [SUR-62]GapClosed (fetchAllCloud(since) now wired through lastSyncRef.current with backfill in useAuth.js:189-210)n/aNone — keep the backfill regression covered by incremental-sync.test.
9. Outbox single queue, opaque failure mode — one stuck payload (RLS reject, oversized sourceMeta) blocks the rest; user sees no quarantine surfaceRiskOpenUnknown (predates 2026-04-23)Split the outbox by entity type or add a poison-message quarantine + a Settings panel that lists stuck items with retry/discard actions.
10. No OCR regression coverage for callTranscribeImageGapClosed (covered by api.test.js, photoAdapter.test.js, capture.test.jsx)n/aNone.
11. Accessibility gaps — long-press only for edit/delete, no keyboard / context-menu / visible button alternative; bottom-nav-hidden state on capture/note compounds itGapOpenUnknown (predates 2026-04-23)Add a visible “more actions” overflow button on note rows + keyboard shortcuts; document target sizes against WCAG 2.5.5.
12. Usage opacity — ai_usage_daily not surfaced; users only see “Monthly limit reached” toast at the capGapOpenUnknown (predates 2026-04-23)Add a usage meter (“X / 50 calls this month”) in ProfileScreen or SettingsModal, sourced from ai_usage_daily and the resolved per-user user_profiles.month_limit.
13. SUR-209 — broken in-app help links (/#faq, /#what-is-surfc resolve to nothing post-SUR-218)RiskClosed (SUR-223 Phase 1 — ProfileScreen and SettingsModal now point to https://help.surfc.app)n/aPhase 2 (in-app help renderer reading from docs/getting-started/) remains outstanding — track separately.
14. How-It-Works placeholders — removed by SUR-215RiskClosed (already in prior report)n/aNone.
15. Dexie ↔ Supabase lockstep is human-driven — Dexie v1→v9 migration chain not cross-checked against the schema contractAssumptionOpenUnknown (predates 2026-04-23)Extend scripts/check-schema.js to also assert Dexie’s expected columns; add a PR template item for “Dexie + Supabase + mergeCloudRecords updated together”.
16. Free-tier monthly cap is per-user via user_profiles.month_limit (SUR-92), default 50 + optional allocation_override; per-token cost not yet correlated with ai_usage_daily.input_tokens/output_tokensAssumptionOpen3 (refreshed 2026-04-29 with SUR-92 source-doc update)Pull token-cost data from ai_usage_daily.input_tokens/output_tokens into a simple weekly cost dashboard; add a per-user resolved-limit cache (30–60 s) inside the Edge Function once managed-AI traffic grows ≥ 10×; revisit once paid tiers land.
17. VitePWA autoUpdate is acceptable — no onNeedRefresh UI; users won’t be told a new version existsAssumptionOpenUnknown (predates 2026-04-23)Wire a registerSW({ onNeedRefresh }) toast with a “Reload” CTA; combine with fix for deprecated apple-mobile-web-app-capable meta.
18. Cascading book + note tombstones acceptable despite orphan-image risk on failed image-upload racesAssumptionOpenUnknown (predates 2026-04-23)Same fix as #7 — once orphan-image sweep exists, this assumption can be retired.
19. Future ingest adapters (Readwise / Kindle) reuse the existing ingest interface without backend changesAssumptionOpenUnknown (predates 2026-04-23)Build a Readwise PoC against the current adapter to validate; if backend changes are needed, surface them before adapter v2 lands.
20. CaptureFabMenu speed-dial provides sufficient navigation on capture/note views with bottom-nav hidden [SUR-238]AssumptionOpen9 (added 2026-04-23)Run a 5-user usability test on capture → save → return-to-library; if friction shows, add a persistent back affordance.
21. How are Supabase credentials and ANTHROPIC_API_KEY managed in Netlify and Supabase Edge environments — and what is the rotation cadence?DependencyOpenUnknown (predates 2026-04-23)Document the secret-management process in docs/architecture/DEPLOYMENT_ARCHITECTURE.md; agree a rotation cadence (90 days) and put a calendar reminder.
22. Monitoring / alerting for sync failures and Edge Function errors — console.log-based, not piped to PagerDuty / Slack / emailGapOpenUnknown (predates 2026-04-23)Add a Logflare or Logtail drain on the Edge Functions; alert on >N 5xx/min and on decrypt_failure PostHog event spikes.
23. Should exports include Supabase storage paths or binary blobs to guarantee a fully portable backup?GapOpenUnknown (predates 2026-04-23)Add an opt-in “Include images” toggle to buildExport; emit a zip with JSON + image blobs when enabled.
24. SLA for clearing orphaned note-images on still-active accounts (deleted notes, replaced images)GapOpenUnknown (predates 2026-04-23)Define a 7-day orphan window; bundle into the same nightly sweep proposed in #7.
25. Multi-device editing conflict UI beyond last-write-wins not surfaced — mergeCloudRecords silently picks higher updated_atGapOpenUnknown (predates 2026-04-23)Log conflicts to a local audit table; surface a “Your edit was overwritten” toast with a “Restore my version” link.
26. Background sync (service-worker sync events) for queued writes when app is closedGapOpenUnknown (predates 2026-04-23)Register a background-sync event in the VitePWA service worker; gate behind a feature-flag for browsers that support it.
27. Edge Function debug logs include user_id on every authenticated request — broader audit trail than the rest of the appRiskOpen6 (added 2026-04-26)Strip or hash user.id in anthropic-proxy/index.ts:399-400, 423; keep only error-path logs at warn/error level.
28. Rate-limit race window — getMonthlyUsage check and recordUsage write are not atomic; documented worst-case overrun equal to in-flight requests; tightens once paid tiers landRiskOpen6 (added 2026-04-26)Move enforcement into a CHECK on the upsert_ai_usage RPC, or wrap in a SELECT … FOR UPDATE; revisit once paid tiers land.
29. Full re-decrypt on every sync — loadAll(decryptFn) re-decrypts every encrypted note text after each merge; sync time scales linearly with library sizeRiskOpen6 (added 2026-04-26)Build an encryption-only delta path keyed off updatedAt; batch decrypts with a Web Worker; add a progress indicator.
30. Sync image fetch is sequential — for loop, O(N) round-trips on a fresh device; failures silently swallowedRiskOpen6 (added 2026-04-26)Parallelise with Promise.allSettled and a small concurrency limit (e.g. 6); surface per-image failures as a retry list.
31. Decrypt-failure recovery is dead-end — flagged with decryptError: true, no UI retry path beyond full sign-out, no count surfaced to userGapOpen6 (added 2026-04-26)Add a “Decryption issues” panel in Settings showing affected count + a “Retry unlock” button that re-runs getEncryptionPrfOutput.
32. Transfer-code auto-expire is best-effort — 60 s deactivation upsert can fail silently, leaving wrapper active until next create cycle (90 s effective redemption window with the 30 s clock-skew buffer)RiskOpen6 (added 2026-04-26)Add a server-side scheduled job (Supabase pg_cron) to deactivate transfer-v1 blobs older than 90 s; alert on backlog growth.
33. No PWA update prompt — VitePWA autoUpdate with no onNeedRefresh UI; iOS standalone install path most exposedRiskOpen6 (added 2026-04-26)Same fix as #17 — implement onNeedRefresh toast and update the deprecated meta tag in index.html.
34. db.js has no dedicated unit tests — CRUD helpers and v1→v9 Dexie upgrade chain only covered transitivelyGapOpen6 (added 2026-04-26)Add src/test/db.test.js covering each migration step + cascade behaviours; add a CI gate that runs migration replay against a fixture DB.
35. Duplicate-source UX gap [SUR-257] — duplicate hint is non-blocking, no merge / de-dupe path; orphan sources accumulateGapOpen6 (added 2026-04-26)Make the hint a confirm-step (“Looks like a duplicate — Use existing / Create anyway”); add an admin de-dupe tool in Settings.
36. Branded auth-email template lives in repo only [SUR-261] — magic-link.html must be manually pasted into Supabase dashboard with no CI syncGapOpen6 (added 2026-04-26)Add a scripts/sync-auth-templates.js using the Supabase Management API; gate on a manual approval but make it one command.
37. Approved waitlist rows with user_id IS NULL stranded for email-OTP [SUR-261] — match_waitlist_on_signup only back-fills on auth.users INSERT and OTP refuses shouldCreateUser:falseGapOpen6 (added 2026-04-26)Add a nightly job that runs the audit query (status='approved' AND user_id IS NULL) and auto-sends Supabase invites; alert on backlog.
38. Email-OTP error-string mapping is best-effort [SUR-261] — relies on Supabase Auth’s Signups not allowed for otp substring staying stableAssumptionOpen6 (added 2026-04-26)Add an integration test that pings the live Supabase Auth response for an unknown email and asserts the substring; alert on regression.
39. Email signups stay disabled at the Supabase platform level — requestEmailOtp pins shouldCreateUser:false as defence-in-depthAssumptionOpen6 (added 2026-04-26)Document the dashboard toggle state in DEPLOYMENT_ARCHITECTURE.md; add a check to scripts/check-schema.js that asserts the auth config.
40. Email-OTP code length and expiry are Supabase defaults (6 digits / 1 hour)AssumptionOpen6 (added 2026-04-26)Acceptable for personal scale; revisit if abuse signals appear (failed-attempt rate-limit telemetry).
41. isStandaloneOrTwa() decides OTP-code vs magic-link delivery once at component mount [SUR-261] — no hot-swap mid-sessionAssumptionOpen6 (added 2026-04-26)Acceptable today; add a comment in src/lib/platform.js explaining the constraint and revisit when iOS/Android expose surface hot-swapping.
42. Cross-domain help.surfc.app (Cloudflare Pages) remains reachable from authenticated app shell links — no in-app fallback if DNS / cert / Pages outageAssumptionOpen6 (added 2026-04-26)Phase 2 in-app help renderer (already on backlog) addresses this; add a temporary fallback message on link click.
43. session.access_token expiry handled by getSession() calling _callRefreshToken immediately before each Edge Function invokeAssumptionOpen6 (added 2026-04-26)Add a regression test that simulates a backgrounded-PWA “Invalid JWT” and asserts the refresh path fires.
44. ANTHROPIC_API_KEY rotation is a manual operator task — no client-side hint of expiry; rotated-then-misconfigured key surfaces as blanket 500AssumptionOpenUnknown (predates 2026-04-23)Same fix as #5 — startup self-check Edge Function with structured error codes.
45. Project-level Anthropic spend cap unknown — only per-user ai_usage_daily counters; no automated alertDependencyOpenUnknown (predates 2026-04-23)Set a project-level monthly cap in Anthropic Console; pipe spend metrics into PostHog or a weekly digest email.
46. Account recovery without passkey + no transfer code — data unrecoverable by design; copy points to “the device where you originally enabled encryption”GapOpenUnknown (predates 2026-04-23)Document this as a deliberate trade-off in the help site; consider an opt-in “recovery key” downloaded at enrolment for users who accept the risk.
47. Deletion-success audit trail beyond in-flight delete-account 200 response — if auth.admin.deleteUser succeeds but client crashes before finalizeAccountDeletion, user sees stale sessionGapOpenUnknown (predates 2026-04-23)Add a server-side account_deletions audit table written inside the Edge Function transaction; reconcile on next sign-in attempt.
48. Prompt-injection defence is partial [SUR-242] — Phase 1 system-prompt fencing live; Azure Content Safety (Phase 2) outstandingRiskClosed (Phase 2 has now landed in the source doc on 2026-05-01 — the anthropic-proxy/guardrail.ts Azure Prompt Shields + Spotlighting + harm moderation pipeline is wired in, with input shield, transcription-post Spotlighting, and output harm classifier). The residual risks are now tracked at higher granularity in items #52–#54 + #56.n/aNone — keep this row strikethrough so row indices stay stable; new SUR-242 risks tracked at #52–#54 + #56.
49. Dual-hosting transitional period [SUR-254] — surfc-web/ simultaneously configured for Netlify and Cloudflare Pages; cache-header / 301 changes must be applied to bothRiskNot in main on 2026-05-02 (sibling-repo concern; not present in this repo’s RISKS_GAPS_ASSUMPTIONS.md)n/aTrack in the surfc-web/ repo’s own RAID instead; remove from this report unless and until the source doc on main re-introduces it.
50. Supabase Edge runtime uses a single Anthropic API key with no per-org isolation and no project-level spend capAssumptionOpenUnknown (predates 2026-04-23)Treat per-user ai_usage_daily + PostHog as the early-warning signal until paid tiers land; pair with #45 to add a project-level cap in Anthropic Console; revisit org isolation once multi-tenant becomes a thing.
51. Per-request quota lookup latency [SUR-92] — extra indexed single-row SELECT on user_profiles per managed-AI call (≈ 1–3 ms warm); becomes a concern only at sustained tens-of-requests-per-secondRiskOpen3 (added 2026-04-29 with the SUR-92 source-doc update)Add a 30–60 s in-memory {userId → resolvedLimit} cache in the Edge Function once managed-AI traffic grows ≥ 10× current volume; not needed for v1.5.
52. Azure Content Safety fail-open gap [SUR-242] — guardrail.ts fails open silently on Azure 5xx / network error / timeout / missing env; managed AI calls still succeed and _failOpen:true is appended to the response, but no PostHog event, Supabase metric, or alerting fires when injection/harm protection is silently inactiveRiskOpen1 (added 2026-05-01 with the SUR-242 source-doc update)Emit a guardrail_fail_open PostHog event from the Edge Function; surface a daily count in PostHog dashboards; alert if hourly fail-open rate exceeds N%; consider a GUARDRAIL_REQUIRED env flag for fail-closed mode in production once stable.
53. Azure Content Safety latency budget per call [SUR-242] — each managed request runs up to 3 sequential Azure calls (shield(input) → Anthropic → spotlight(transcription)moderate(output)); under Azure 429-retry × 5 attempts with exponential back-off the wall-clock can add ~5–15 s before fail-open kicks in. Per-call timeout is not documented in guardrail.tsRiskOpen1 (added 2026-05-01 with the SUR-242 source-doc update)Document the per-call Azure timeout in guardrail.ts; cap retries at 2 (not 5) before falling open; consider parallelising input-shield with the Anthropic call where order is not strict; add a guardrail_latency_ms PostHog metric.
54. Client-side PII detection is structural/regex-only [SUR-242] — src/safety/piiRegex.js covers card/Luhn, IBAN/mod-97, NIN, SSN, phone E.164, email; checkNerPii() and checkPromptInjection() are exported stub no-ops deferred to SUR-246. v1.4 policy is warn-not-block — unstructured PII (names, addresses, free-text medical info) passes through to Anthropic and is stored in Dexie/Supabase in plaintext-of-the-encrypted-note (i.e. inside the encrypted blob, but visible to the AI proxy)GapOpen1 (added 2026-05-01 with the SUR-242 source-doc update)Land SUR-246 (on-device Llama Prompt Guard 2 + GLiNER NER) — keeps detection local, avoids privacy regression; until then add a clearer “AI sees your note text — review before sending” disclaimer next to the BottomSheet review prompt.
55. Blog requires full rebuild per post [SUR-256] — surfc-web/src/content/blog/ MDX is baked into the static dist/ at build time; no CMS, draft preview URL, or incremental rebuild path. Typo fixes trigger a full site rebuild + deployGapOpen1 (added 2026-05-01 with the SUR-256 source-doc update)Acceptable at solo-founder posting cadence; revisit if cadence ≥ 1 post/week. Possible mitigations: Cloudflare Pages preview deploys per branch (already exists for surfc-web/); a thin draft-preview layer using Astro’s import.meta.env.DEV flag.
56. Azure Content Safety severity threshold ≥5 calibrated for Surfc content [SUR-242] — moderate() blocks on Hate/Violence/Sexual/SelfHarm at severity ≥5; threshold selected against a representative book-annotation/philosophical-passage corpus during the SUR-242 spike. May be too permissive (severity-4 harms pass through) or too aggressive (literary/historical passages with moderate-out-of-context scores) at scaleAssumptionOpen1 (added 2026-05-01 with the SUR-242 source-doc update)Once managed usage exceeds ~500 calls/month, sample blocked + passed calls and compute false-positive / false-negative rates against a small holdout set; adjust threshold per-category before v2.0.

Summary

  • 56 items total in this report (51 in the prior 01-05-2026 report → +5 new for SUR-242 / SUR-256). One item (#48) flipped from “Not in main” to Closed.
  • Closed since last report: 1 — item #48 (Prompt-injection defence is partial [SUR-242]) flipped from “Not in main” to Closed because the SUR-242 Phase-2 Azure Content Safety pipeline is now wired into the source doc on main. The 6 prior strikethroughs (#3, #6, #8, #10, #13, #14) remain closed.
  • New items added since 01-05-2026: 5 — #52 Azure Content Safety fail-open gap (SUR-242), #53 Azure Content Safety latency budget per call (SUR-242), #54 Client-side PII detection regex-only (SUR-242), #55 Blog requires full rebuild per post (SUR-256), #56 Azure Content Safety severity threshold ≥5 calibrated for Surfc content (SUR-242).
  • Removed/struck-through since last report: None new beyond #48 flipping to Closed. Item #49 (SUR-254 dual-hosting) remains “Not in main” with strikethrough — same status as the prior report.
  • Days-open updated: All open items aged by 1 day. Item dated 2026-04-23 (#20) → 9 days; items dated 2026-04-26 (#27–#43) → 6 days; SUR-92 quota assumption (#16) and SUR-92 latency (#51) → 3 days; the new SUR-242 / SUR-256 rows (#52–#56) → 1 day; pre-2026-04-23 items still report Unknown because the source document does not record a per-item Logged date.
  • By type (open totals): 13 Risks (was 12 — added #52, #53; #48 closed), 18 Gaps (was 16 — added #54, #55), 4 Dependencies (unchanged), 14 Assumptions (was 13 — added #56). Plus 7 Closed and 1 “Not in main” row held with strikethrough.

Notes / autonomous decisions

  • Read the source from the working tree at docs/architecture/RISKS_GAPS_ASSUMPTIONS.md. The sandbox could not run git log against the local checkout (the Explore agent reported the sandbox repo had no commit history reachable), so I could not verify the source against origin/main:… as in prior runs. I treated the working tree as the current main per the source doc’s own change-summary banner, which records the SUR-242 + SUR-256 updates as dated 2026-05-01. If a manual git diff origin/main -- docs/architecture/RISKS_GAPS_ASSUMPTIONS.md shows the working tree is ahead of main, items #52–#56 should be flagged “Not in main” on the next run rather than counted as open today.
  • Item #48 is marked Closed (rather than Removed) because the prior report explicitly held it as “Not in main”; the row is preserved with strikethrough so row indices stay stable. The residual exposure that remains after Phase 2 (fail-open behaviour, latency, calibration, structural-only PII) is captured at higher granularity in #52–#54 + #56.
  • Item #49 (SUR-254) is kept as “Not in main” with strikethrough for the same row-stability reason. Re-instate without renumbering if it appears in the source doc on a future run.
  • Suggested actions are concrete and ticket-sized; they are suggestions, not commitments.
  • Delivery: The task asked for the report to be sent to a chat in the Surfc project on claude.ai. The Chrome MCP extension was not connected at run time (list_connected_browsers returned []), so auto-delivery to a claude.ai chat was not possible. The report has been saved to docs/architecture/RAID_REPORT_2026-05-02.md so it can be pasted into the chat manually, or auto-posted on the next run if the Chrome extension is connected.