Skip to content

RAID Report — 2026-05-03

Date: 03-05-2026 Subject: RAID Report

Source: docs/architecture/RISKS_GAPS_ASSUMPTIONS.md on main (resolved via git show main:…, 68 lines, latest in-doc update dated 2026-04-26 covering the SUR-92 source-doc rewrite and the SUR-261 email-OTP additions). Previous report: RAID_REPORT_2026-05-02.md (1 day ago). Note: the working tree on the current feature branch contains five additional change-summary entries dated 2026-04-29 / 2026-04-30 / 2026-05-01 (SUR-92 fixes refresh banner, the assetlinks credential-sharing note, the SUR-242 Azure Content Safety entries, and the SUR-256 blog rebuild gap). None of those banner updates have merged to main yet, so this report — per the task brief — is sourced from main and treats those rows as Not in main for row stability.

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_tokensAssumptionOpen4 (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]AssumptionOpen10 (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 appRiskOpen7 (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 landRiskOpen7 (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 sizeRiskOpen7 (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 swallowedRiskOpen7 (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 userGapOpen7 (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)RiskOpen7 (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 exposedRiskOpen7 (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 transitivelyGapOpen7 (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 accumulateGapOpen7 (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 syncGapOpen7 (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:falseGapOpen7 (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 stableAssumptionOpen7 (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-depthAssumptionOpen7 (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)AssumptionOpen7 (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-sessionAssumptionOpen7 (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 outageAssumptionOpen7 (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 invokeAssumptionOpen7 (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 (held with strikethrough for row stability — prior report flipped this to Closed against the working tree where the SUR-242 Phase-2 pipeline is wired in anthropic-proxy/guardrail.ts. Important caveat: that change has not yet merged to main as of 2026-05-03, so on main the underlying SUR-242 entry simply does not exist; the residual risks #52–#54 + #56 likewise remain “Not in main” — see notes below.)n/aNone — keep this row strikethrough; revisit status on the next run once the SUR-242 / SUR-256 banner updates land on main.
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-03 (sibling-repo concern; not present in this repo’s RISKS_GAPS_ASSUMPTIONS.md on main)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-secondRiskOpen4 (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 inactiveRiskNot in main on 2026-05-03 (the SUR-242 source-doc update has not merged to main yet — present only in working tree of feature/sur-233-linked-d)n/aRe-instate as Open without renumbering once the SUR-242 banner update lands on main. Suggested action then: 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.tsRiskNot in main on 2026-05-03 (same reason as #52 — SUR-242 banner update not merged to main)n/aRe-instate when SUR-242 lands on main. Suggested action then: 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)GapNot in main on 2026-05-03 (same reason as #52 — SUR-242 banner update not merged to main)n/aRe-instate when SUR-242 lands on main. Suggested action then: 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 + deployGapNot in main on 2026-05-03 (the SUR-256 source-doc update has not merged to main yet — present only in working tree of feature/sur-233-linked-d; sibling-repo deployment concern)n/aRe-instate when SUR-256 lands on main. 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 scaleAssumptionNot in main on 2026-05-03 (same reason as #52 — SUR-242 banner update not merged to main)n/aRe-instate when SUR-242 lands on main. Suggested action then: 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, matching the 02-05-2026 row count (no new items raised in the 24 h window when sourcing strictly from main).
  • Closed since last report: 0 — none of the open items have been resolved on main between 2026-05-02 and 2026-05-03. The 7 prior strikethroughs (#3, #6, #8, #10, #13, #14, #48) remain Closed.
  • New items added since 02-05-2026: 0 (against main). The five SUR-242 / SUR-256 rows that the prior report counted as new (#52–#56) are Not in main today and are held with strikethrough for row stability — see the divergence note below.
  • Removed/struck-through since last report: None new beyond what was already marked.
  • Days-open updated: All open items aged by 1 day. Item dated 2026-04-23 (#20) → 10 days; items dated 2026-04-26 (#27–#43) → 7 days; SUR-92 quota assumption (#16) and SUR-92 latency (#51) → 4 days; pre-2026-04-23 items still report Unknown because the source document does not record a per-item Logged date.
  • By type (open totals on main): 11 Risks (was 13 — #52, #53 flipped to Not in main), 16 Gaps (was 18 — #54, #55 flipped to Not in main), 4 Dependencies (unchanged), 13 Assumptions (was 14 — #56 flipped to Not in main). Plus 7 Closed and 6 “Not in main” rows held with strikethrough (#48, #49, #52, #53, #54, #55, #56 — counting #48 separately as Closed-against-working-tree-only).

Notes / autonomous decisions

  • Source resolution. The task brief said “Use the version on the main branch.” git show main:docs/architecture/RISKS_GAPS_ASSUMPTIONS.md returned the 68-line file last touched by commit 895cd1a (chore(quota): per-user managed-AI monthly limit via user_profiles [SUR-92]). The latest commit on main overall is ebbac96 from 2026-05-02 16:03 +0200. The five 2026-04-29 / 2026-04-30 / 2026-05-01 banner updates the 02-05-2026 report referenced are present in the working tree on the current feature branch (feature/sur-233-linked-d) but have not merged to main yetgit diff confirms 70 lines in working tree vs 68 lines on main, with the additional banner entries and no underlying body-text change.
  • Divergence handling. Items #52–#56 (Azure Content Safety + blog rebuild) were treated as Open in the 02-05-2026 report because that run could not access git and read the working tree directly. Today’s run, sourcing from main per the brief, marks them as Not in main with strikethrough — same convention previously used for #49. Row indices are preserved so a side-by-side compare against the 02-05-2026 report stays clean.
  • #48 ambiguity. The prior report flipped #48 from “Not in main” to Closed on the assumption that SUR-242 Phase 2 had landed in the source doc. On main today, neither the SUR-242 entry nor the Phase-2 implementation banner is present. I’ve kept #48 with its prior Closed status for row-stability and noted the caveat inline; the next run should re-evaluate once the SUR-242 / SUR-256 banner updates merge.
  • Linear cross-check not performed. The task brief did not specifically require a Linear status pull for cross-referencing closures; the source RAID doc is treated as the canonical record. If a future run should hit the Linear MCP for ticket-level closure tracking (e.g. SUR-IDs flipping to Done), flag that as a follow-up.
  • 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-03.md so it can be pasted into the chat manually, or auto-posted on the next run if the Chrome extension is connected.