RAID Report — 2026-05-04
Date: 04-05-2026 Subject: RAID Report
Source: docs/architecture/RISKS_GAPS_ASSUMPTIONS.md on main (resolved via git show main:…, 80 lines, last touched by commit 013b883 on 2026-05-03 — chore: bundle outstanding docs and tighten .gitignore). Previous report: RAID_REPORT_2026-05-03.md (1 day ago). Major change since the prior report: the SUR-242, SUR-256, SUR-233, and SUR-235 banner entries that had been working-tree-only have now merged to main, so rows #52–#56 are no longer “Not in main” and two new rows (#57 SUR-233 device_label immutability and #58 SUR-235 add-idea cap TODO) are added with Days Open = 2 per the source-doc 2026-05-02 banner.
| Issue Number | Issue Type | Status | Days Open | Suggested Action |
|---|---|---|---|---|
1. Monolithic UI/control plane (src/App.jsx ~500 lines, owns auth gates, layout, route tree, prop fan-out) | Risk | Open | Unknown (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) | Risk | Open | Unknown (predates 2026-04-23) | Decompose into UI-state hook + pure persistence module + ingest/PostHog dispatcher; add unit tests around the seams. |
| 3. | Risk | Closed (resolved per 2026-04-26 audit — useAuth.js:175 only sets the flag after success) | n/a | None — 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] | Dependency | Open | Unknown (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 toast | Dependency | Open | Unknown (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. | Risk | Closed (SUR-91 fully sunset; only a one-time apiKey meta cleanup remains in db.js:156) | n/a | None — 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) | Gap | Open | Unknown (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. fetchSince helper unused [SUR-62] | Gap | Closed (fetchAllCloud(since) now wired through lastSyncRef.current with backfill in useAuth.js:189-210) | n/a | None — 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 surface | Risk | Open | Unknown (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. callTranscribeImage | Gap | Closed (covered by api.test.js, photoAdapter.test.js, capture.test.jsx) | n/a | None. |
| 11. Accessibility gaps — long-press only for edit/delete, no keyboard / context-menu / visible button alternative; bottom-nav-hidden state on capture/note compounds it | Gap | Open | Unknown (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 cap | Gap | Open | Unknown (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. /#faq, /#what-is-surfc resolve to nothing post-SUR-218) | Risk | Closed (SUR-223 Phase 1 — ProfileScreen and SettingsModal now point to https://help.surfc.app) | n/a | Phase 2 (in-app help renderer reading from docs/getting-started/) remains outstanding — track separately. |
| 14. | Risk | Closed (already in prior report) | n/a | None. |
| 15. Dexie ↔ Supabase lockstep is human-driven — Dexie v1→v9 migration chain not cross-checked against the schema contract | Assumption | Open | Unknown (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_tokens | Assumption | Open | 5 (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 exists | Assumption | Open | Unknown (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 races | Assumption | Open | Unknown (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 changes | Assumption | Open | Unknown (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] | Assumption | Open | 11 (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? | Dependency | Open | Unknown (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 / email | Gap | Open | Unknown (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? | Gap | Open | Unknown (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) | Gap | Open | Unknown (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_at | Gap | Open | Unknown (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 closed | Gap | Open | Unknown (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 app | Risk | Open | 8 (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 land | Risk | Open | 8 (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 size | Risk | Open | 8 (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 swallowed | Risk | Open | 8 (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 user | Gap | Open | 8 (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) | Risk | Open | 8 (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 exposed | Risk | Open | 8 (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 transitively | Gap | Open | 8 (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 accumulate | Gap | Open | 8 (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 sync | Gap | Open | 8 (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:false | Gap | Open | 8 (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 stable | Assumption | Open | 8 (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-depth | Assumption | Open | 8 (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) | Assumption | Open | 8 (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-session | Assumption | Open | 8 (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 outage | Assumption | Open | 8 (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 invoke | Assumption | Open | 8 (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 500 | Assumption | Open | Unknown (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 alert | Dependency | Open | Unknown (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” | Gap | Open | Unknown (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 session | Gap | Open | Unknown (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. | Risk | Closed (SUR-242 Phase 2 pipeline now in main via anthropic-proxy/guardrail.ts; the residual gaps are tracked separately as #52, #53, #54, #56) | n/a | None for this row — the residual concerns now live in #52–#56. |
49. surfc-web/ simultaneously configured for Netlify and Cloudflare Pages; cache-header / 301 changes must be applied to both | Risk | Not in main on 2026-05-04 (sibling-repo concern; not present in this repo’s RISKS_GAPS_ASSUMPTIONS.md on main) | n/a | Track 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 cap | Assumption | Open | Unknown (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-second | Risk | Open | 5 (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 inactive | Risk | Open | 3 (added 2026-05-01; landed on main with commit 013b883 on 2026-05-03) | 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.ts | Risk | Open | 3 (added 2026-05-01; landed on main with commit 013b883 on 2026-05-03) | 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) | Gap | Open | 3 (added 2026-05-01; landed on main with commit 013b883 on 2026-05-03) | 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 + deploy | Gap | Open | 3 (added 2026-05-01; landed on main with commit 013b883 on 2026-05-03) | 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 scale | Assumption | Open | 3 (added 2026-05-01; landed on main with commit 013b883 on 2026-05-03) | 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. |
57. device_label is immutable after enrolment [SUR-233] — wrapped_key_blobs.device_label is set once by getDeviceLabel() at passkey-enrolment and never updated; OS-level rename or UA-string change leaves the stored label stale (cosmetic only — crypto unaffected) | Gap | Open | 2 (added 2026-05-02; landed on main with commit 013b883 on 2026-05-03) | Add an authenticated PATCH path on wrapped_key_blobs (RLS scoped by user_id) and surface a “Rename device” affordance in LinkedDevicesModal; keep the original label as a device_label_initial audit column. |
58. Add-idea Free-tier cap not yet enforced [SUR-235 TODO] — AddIdeaSheet “Create” CTA has a TODO(SUR-235) to gate by useEntitlements() for Free-tier custom-idea cap + Pro upsell; today all users can create unlimited custom ideas from both Profile and the note-form sheet | Gap | Open | 2 (added 2026-05-02; landed on main with commit 013b883 on 2026-05-03) | Land SUR-235 — add useEntitlements().canCreateCustomIdea check, replace the bare CTA with a paywall-aware variant, and emit a paywall_block PostHog event when the cap fires; add a tier-cap unit test. |
Summary
- 58 items total in this report (was 56 on 02-05-2026 / 03-05-2026). Two new rows added (#57 SUR-233
device_label, #58 SUR-235 add-idea cap). - Closed since last report: 1 — #48 (Prompt-injection defence is partial — SUR-242 Phase 2 pipeline now on
main). The 7 prior strikethroughs (#3, #6, #8, #10, #13, #14) remain Closed. - New items added since 03-05-2026: 2 — #57 SUR-233
device_labelimmutability and #58 SUR-235 add-idea Free-tier cap TODO. Both added to the source doc by the 2026-05-02 banner and landed onmainvia commit013b883on 2026-05-03. - Status flips since 03-05-2026: 5 rows flipped from “Not in main” → Open after commit
013b883landed (#52, #53, #54, #55, #56). #49 (SUR-254 dual-hosting) remains “Not in main” (sibling-repo concern). - Days-open updated: All open items aged by 1 day. Item dated 2026-04-23 (#20) → 11 days; items dated 2026-04-26 (#27–#43) → 8 days; SUR-92 quota assumption (#16) and SUR-92 latency (#51) → 5 days; SUR-242 / SUR-256 items (#52, #53, #54, #55, #56) → 3 days; SUR-233 / SUR-235 items (#57, #58) → 2 days; pre-2026-04-23 items still report
Unknownbecause the source document does not record a per-item Logged date. - By type (open totals on
main): 13 Risks (was 11 — #52, #53 flipped from Not in main → Open), 18 Gaps (was 16 — #54, #55, #57, #58 flipped/added; #48 Closed nets the gap count to +2), 4 Dependencies (unchanged), 14 Assumptions (was 13 — #56 flipped from Not in main → Open). Plus 7 Closed (was 6 — #48 added) and 1 “Not in main” row held with strikethrough (#49).
Notes / autonomous decisions
- Source resolution.
git show main:docs/architecture/RISKS_GAPS_ASSUMPTIONS.mdreturned the 80-line file last touched by commit013b883(chore: bundle outstanding docs and tighten .gitignore, 2026-05-03). This is the same commit that brought the SUR-233, SUR-235, SUR-242, SUR-256, and 2026-04-29/30/05-01 banner entries ontomain. Compared to yesterday’s run, the source doc grew from 68 → 80 lines and the SUR-233 / SUR-235 rows are net-new. - #48 status flip. Prior report kept #48 (Prompt-injection defence is partial) Closed against the working tree but flagged a caveat that it was not yet on
main. Today’s main confirms the SUR-242 Phase 2 pipeline (anthropic-proxy/guardrail.ts+ the SUR-242 banner items) is inmain, so #48 is now confirmed Closed without the caveat. The residual SUR-242 risks live as #52, #53, #54, and #56. - #49 unchanged. The SUR-254 dual-hosting row is still not in this repo’s
RISKS_GAPS_ASSUMPTIONS.mdonmain— it is asurfc-web/concern. Held with strikethrough for row stability. - Linear cross-check not performed. The task brief did not specifically require a Linear status pull; the source RAID doc on
mainis treated as the canonical record. If a future run should hit Linear 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_browsersreturned[]), so auto-delivery to a claude.ai chat was not possible. The report has been saved todocs/architecture/RAID_REPORT_2026-05-04.mdso it can be pasted into the chat manually, or auto-posted on the next run if the Chrome extension is connected.