Technology Stack
Technology Stack
CHANGE SUMMARY
- Updated: Added the managed Anthropic proxy stack component (Supabase Edge Function + ai_usage tracking) and how the client routes between BYOK and managed modes (
src/api.js,src/supabase.js,supabase/functions/anthropic-proxy/index.ts).- Updated: Clarified that Anthropic secrets now live server-side for managed users, while BYOK still sends prompts directly.
- Updated (2026-04-23): Added
surfc-web/Astro marketing stack section reflecting SUR-218 dual-repo split and SUR-215 legacy component removal.- Updated (2026-04-26, SUR-261): Authentication surface expanded — Google OAuth + email-OTP sign-in, platform-aware OTP-code vs magic-link delivery via
src/lib/platform.js, branded email template atsupabase/email-templates/magic-link.html.- Updated (2026-04-27, SUR-254):
surfc-web/static assets section updated —public/_headersadded for Cloudflare Pages,.nvmrcpinning Node 22.- Updated (2026-04-30):
public/.well-known/assetlinks.jsonnow covers credential sharing (delegate_permission/common.get_login_creds) for the Android TWA alongside URL-handling.- Updated (2026-05-01, SUR-242): Azure AI Content Safety added as a guardrail dependency (
guardrail.tsinanthropic-proxy). Client-side safety module added (src/safety/).- Updated (2026-05-01, SUR-256):
surfc-web/blog stack documented — MDX content collections, RSS, reading-time plugin, rehype autolink headings;@astrojs/cloudflareadapter intentionally absent (static output).- Updated (2026-05-04, SUR-303 refactor/sur-303-extract):
anthropic-proxy/prompts.tsextracted as single source of truth for managed-AI system prompts;GREAT_IDEASnow sourced fromsrc/constants.jsvia Deno cross-tree import (constants.js must remain Vite-neutral).__tests__/prompts.test.tsadded.src/constants.jsheader comment added documenting the Vite-neutral constraint.- Updated (2026-05-02, SUR-237): Migration 0014 adds server-stamped
DEFAULTtowrapped_key_blobs.created_atand introducesselect_fresh_transfer_blobSECURITY DEFINER RPC.upsertWrappedKeyBlobdropscreatedAtfor new inserts;redeemDeviceTransferuses the RPC for freshness checks. Client-sideTRANSFER_MAX_AGE_MS/TRANSFER_MAX_FUTURE_SKEW_MSconstants removed;TRANSFER_SANITY_FUTURE_SKEW_MS(5 min) kept as defense-in-depth. Testdevice-transfer-server-time.test.jsreplacesdevice-transfer-stale-blob.test.js.- Updated (2026-05-04, SUR-303): New production deps —
react-markdown^10.1.0,remark-gfm^4.0.1,remark-directive^4.0.0 — for in-app help rendering. Addedsrc/help/infrastructure documentation.AppGates,ShellNavigation, overlay components extracted from App.jsx. New hooks:useMediaQuery,useUserProfile.- Updated (2026-05-03, SUR-233 follow-up):
fetchDeviceListinuseKeyManagement.jsmemoized withuseCallback([session?.user?.id]). Regression testsrc/test/useKeyManagement-stability.test.jsxadded.- Updated (2026-05-02, surfc-web): Fourth blog post published —
surfc-architecture.mdxwithsurfc-detailed-architecture.svgarchitecture diagram. Blog post count updated to four.
This document covers both sibling repos. Stack descriptions are based on code/config evidence only.
flowchart TB UI[React 18 + Vite\nsrc/main.jsx, src/App.jsx] --> Hooks[Stateful Hooks\nsrc/hooks/*] Hooks --> Dexie[Dexie IndexedDB\nsrc/db.js] Hooks --> Supabase[supabase-js client\nsrc/supabase.js] Hooks --> Anthropic[Anthropic HTTP APIs\nsrc/api.js] Hooks --> EdgeFn[Supabase Edge Function\nsupabase/functions/anthropic-proxy] EdgeFn --> Anthropic Dexie --> Storage[Browser storage + PWA\ntokens.css, styles.css, VitePWA] Supabase --> CloudDB[Supabase Postgres/RLS\nsupabase/migrations/*.sql] Supabase --> StorageBucket[Supabase Storage note-images]Client application (presentation + UX)
- React 18 with Vite bundling (
package.json,src/main.jsx,src/App.jsx). - The app shell, navigation, and screen layout live inside
src/App.jsx, which conditionally renders components undersrc/components/and wires gestures viasrc/hooks/useLongPress.js. - Styling is global via
tokens.cssandstyles.css, imported at the root (src/main.jsx). - Vitest + Testing Library assert UI behaviors (
src/test/App.behaviour.test.jsx,src/test/capture.test.jsx,src/test/custom-ideas.test.jsx).
Local state & offline persistence
- Dexie models
books,notes,customIdeas,meta, andoutboxtables with six schema versions and upgrade steps (src/db.js). useAuthhydrates Dexie vialoadAll, exposesbooks/notes/customIdeas/apiKey, tracks connectivity, and coordinates sync (src/hooks/useAuth.js).useNoteForm,useSettings, anduseNoteActionsmutate Dexie entities, manage captured images, export/import archives, and enqueue cloud writes via the sharedcloudWritehelper.src/test/sync.test.jsusesfake-indexeddbto validate merge semantics;src/test/outbox.test.jscovers the queue collapse logic.
Cloud sync & storage
- Supabase JS client handles auth, Postgres tables (
books,notes,custom_ideas), and storage bucketnote-images(src/supabase.js). - SQL schema and RLS policies are defined by the versioned migrations under
supabase/migrations/*.sqlplus the contract inscripts/schema-contract.js;npm run check:schemacallsscripts/check-schema.jsto verify the live database matches. useAuth.cloudWriteattempts upserts immediately, queuing failures in Dexieoutbox;syncFromCloudflushes the queue, fetches Supabase tables viafetchAllCloud, merges them locally (mergeCloudRecords), and backfills missing images viadownloadImage(src/hooks/useAuth.js,src/db.js)..envsuppliesVITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEY, which Vite injects into the client bundle (src/supabase.js).
Authentication
src/AuthScreen.jsxis the entry point — three-zone layout (whitespace top → centred hero → CTA stack pinned above safe-area inset,src/styles.css). The hero is rendered by the sharedsrc/components/HomeHero.jsxso AuthScreen and HomeScreen stay visually identical.- Primary CTA: Google OAuth.
signInWithGoogle()insrc/supabase.jscallssupabase.auth.signInWithOAuth({ provider: 'google' })and redirects back towindow.location.origin. - Secondary CTA: “Use a different account” opens
src/components/BottomSheet.jsxcontaining the email-OTP sign-in flow:requestEmailOtp(email)—signInWithOtpwithshouldCreateUser:false. Picks 6-digit code on TWA / standalone PWA (noemailRedirectTo) vs magic link on desktop (emailRedirectTo: window.location.origin). Platform decided byisStandaloneOrTwa()insrc/lib/platform.js.verifyEmailOtp(email, token)—verifyOtp({ type: 'email' })for the OTP-code path; the magic-link path returns the user signed-in on click.
- Sign-up policy: there is no self-signup. New users come in via Google OAuth (matching an approved
waitlist_requestsrow by email — back-filled by thematch_waitlist_on_signuptrigger insupabase/migrations/0007_waitlist_requests.sql) or via admin-issued Supabase Auth invite. Email-OTP rejects unknown emails at the platform level (shouldCreateUser:false) andAuthScreenmaps Supabase’sSignups not allowed for otperror to a friendly waitlist CTA pointing athttps://surfc.app/waitlist. - Email branding:
supabase/email-templates/magic-link.htmlis the source of truth for the Surfc-branded magic-link / 6-digit-code email; the template branches on{{ if .Token }}to cover both paths and is uploaded manually to Supabase → Authentication → Email Templates.
AI ingestion & tagging
- Anthropic
/v1/messagespowers both transcription and tagging throughcallTranscribeImageandcallDiscoverIdeas. All calls go through the managedanthropic-proxyEdge Function (BYOK sunset, SUR-91). Managed calls post{ action, payload, mimeType?, customIdeas }plus the Supabase session token toinvokeAnthropicProxy(src/supabase.js,supabase/functions/anthropic-proxy/index.ts). supabase/functions/anthropic-proxy/prompts.ts(SUR-303) — extracted module holding all managed-AI system prompt strings:TRANSCRIBE_SYSTEM(the case-1/2/3 image-classification prompt),TRANSCRIBE_SYSTEM_ASCII_NOTE(ASCII-quote postfix), andbuildDiscoverSystem(customIdeas)(Syntopicon assistant builder).GREAT_IDEASis imported fromsrc/constants.jsvia Deno cross-tree import so canonical idea names stay DRY. Both the live Edge Function and the SUR-300 eval harness import from this file.src/ingest/photoAdapter.jscompresses camera captures (src/utils.js), callscallTranscribeImage, and hands normalized notes back touseNoteForm. Manual notes usesrc/ingest/manualAdapter.js.- Tag discovery is triggered from
useNoteForm.discoverIdeasanduseNoteActions.rediscoverIdeas, with results persisted into Dexienotes.tags.
In-app help rendering (SUR-303, 2026-05-04)
react-markdown^10.1.0 — markdown-to-React renderer used byHelpArticleBody. Configured with customcomponentsto rewrite links and render callout nodes.remark-gfm^4.0.1 — GitHub Flavoured Markdown extensions (tables, strikethrough, task lists).remark-directive^4.0.0 — enables:::note,:::tip,:::warning,:::dangercontainer directives in the help article markdown.src/help/manifest.js— readsdocs/getting-started/*.mdat build time viaimport.meta.glob({ query: '?raw', eager: true }); exposes orderedarticlesarray,getArticle(slug), andFORWARD_REFERENCESfor link-validity checks.src/help/calloutPlugin.js— remark plugin that transforms directive container nodes intocalloutAST nodes consumed byHelpArticleBody’scomponentsmap.- Routes
/help(public) and/help/:slug(public) are defined inApp.jsxoutsideAppGates— accessible without authentication.
Client-side safety
src/safety/index.js—runSafetyChecks(text)entry point. Currently runscheckStructuredPiiand returns{ matches: PiiMatch[], blocked: false }.blockedis reserved for SUR-246 hard-block patterns.src/safety/piiRegex.js— six structured-PII patterns (credit card / Luhn, IBAN / mod-97, phone E.164/US/GB, email, UK NIN, US SSN). Stateless; callers consumePiiMatch[]and present the BottomSheet review UI.- Stub seams
checkPromptInjection()andcheckNerPii()are exported no-ops today; SUR-246 will replace the bodies with on-device Llama Prompt Guard 2 and GLiNER NER.
Server-side safety guardrails (SUR-242)
supabase/functions/anthropic-proxy/guardrail.ts— Azure AI Content Safety wrapper. Two public exports:shield(userPrompt, documents?)(Prompt Shields API — detects direct + indirect injection) andmoderate(text)(text:analyze — Hate/Violence/Sexual/SelfHarm harm classifiers at severity ≥5). Written in TypeScript (Deno).- Env vars:
AZURE_CONTENT_SAFETY_ENDPOINT,AZURE_CONTENT_SAFETY_KEY,AZURE_CONTENT_SAFETY_API_VERSION(default2024-09-01). - Both functions fail-open: missing config, Azure 5xx, network error, or timeout →
{ action: 'NONE', failOpen: true }. Azure 429 triggers exponential back-off up to 5 attempts before fail-open. - The guardrail pipeline is integrated into
anthropic-proxy/index.tsas five stages (input shield → Anthropic call → transcription-post spotlight → output moderation → usage record). SeeSYSTEM_ARCHITECTURE.md → Safety guardrail pipelinefor the full flow.
Tooling, build, and tests (surfc/ React app)
- Vite config registers React and VitePWA plugins, sets up JSDOM for Vitest, and configures the manifest + Workbox runtime caching (
vite.config.js). - Netlify deployment uses
npm run build, servesdist/, and rewrites SPA routes plus manifest headers (netlify.toml). - Tests rely on Vitest + Testing Library for UI behavior, Dexie sync unit tests, and Anthropic parser coverage (
package.jsonscripts,src/test/*.test.js[x]). - CSS tokens/variables centralize design primitives in
src/tokens.css, complemented bystyles.css.
surfc-web/ — Astro marketing site (surfc.app)
Added SUR-218 (2026-04-22); legacy LandingPage.jsx and WaitlistScreen.jsx stripped from the React app by SUR-215 (2026-04-23).
flowchart TB AstroSrc[Astro 6 source\nsurfc-web/src/] --> AstroBuild[astro build\ndist/] AstroBuild --> NetlifyMarketing[Netlify static hosting\nsurfc.app] NetlifyMarketing --> Browser[Visitor browser] Browser --> WaitlistFn[supabase/functions/waitlist-signup\n(deployed from surfc/)] WaitlistFn --> SupabaseDB[Supabase Postgres\nwaitlist table]Framework & rendering
- Astro 6 (
astro.config.mjs) — static site generation; zero client-side JS by default. All components are.astrofiles with occasional islands if needed. - No React in
surfc-web/— the two codebases share no component code.
Fonts
- Self-hosted via
@fontsource/*packages (@fontsource/inter,@fontsource/eb-garamond,@fontsource/fira-mono,@fontsource/playfair-display) withfont-display: optionalto avoid layout shift. No Google Fonts CDN calls from the marketing site (SUR-227).
Pages & components
src/layouts/BaseLayout.astro— shared<head>: meta tags, self-hosted fonts, Termly consent, PostHog init (waitlist_submitted,waitlist_duplicate,app_cta_clicked).src/components/WaitlistForm.astro— waitlist sign-up form; posts tosupabase/functions/waitlist-signup/Edge Function (deployed fromsurfc/). Honeypot field namedhp_trap(renamed fromhp_companyin SUR-218 fix to defeat autofill).src/components/{Nav,Hero,ClosingCta,Faq,Footer,…}.astro— landing sections; CTAs tagged withdata-ctaattributes for PostHog attribution.src/components/Nav.astro— collapses into slide-down hamburger on mobile (SUR-228).src/pages/{index,waitlist,policies/*}.astro— routed pages.
Blog (SUR-256, 2026-04-30)
src/content.config.ts— Astro content collection definition forblog; usesglobloader with**/[^_]*.{md,mdx}pattern; Zod schema fields:title,description,pubDate,updatedDate,author,tags,heroImage,heroImageAlt,draft.src/content/blog/— MDX post files; filename (without extension) becomes the URL slug. Four published posts as of 2026-05-04: “The World Doesn’t Reliably Know” (2025-01-02), “A Beginning” (2026-04-15), “Privacy & Piracy” (2026-04-18), “Surfc Architecture” (2026-05-01).src/pages/blog/index.astro— paginated blog index listing published posts sorted by date desc (PAGE_SIZEfromsrc/utils/blog).src/pages/blog/[slug].astro— individual post renderer.src/pages/blog/page/— pagination routes.src/pages/rss.xml.js— RSS feed (@astrojs/rss) exposing all published posts; channel title “Surfc — Founder notes”.src/plugins/remark-reading-time.mjs— injectsminutesReadinto post frontmatter via remark.src/utils/blog.js— shared helpers:isPublishedfilter,sortByDateDesc,PAGE_SIZE.astro.config.mjsupdated: addedmdx()integration,rehype-slug,rehype-autolink-headings(append mode),remarkReadingTime.trailingSlash: 'always'enforced. No@astrojs/cloudflareadapter — static output is flatdist/which Cloudflare Pages publishes directly. The adapter was briefly added in SUR-256 then removed because itsdist/{client,server}split broke Pages publish-dir detection and Lychee link checks.
CI / testing
- Lighthouse CI (
lighthouserc.cjs) — runs on PRs; warns on performance/a11y regressions. Category thresholds only (norecommendedpreset). - Playwright (
playwright.config.ts,tests/) — end-to-end smoke tests against the built site. Termly consent and the waitlist endpoint are mocked in test fixtures. - Lychee link-check — CI step validates no dead internal/external links (
--root-dirmode).
Static assets
public/— images, favicons, OG assets. Long-livedCache-Control: max-age=31536000, immutableheaders for/_astro/*and static assets are set in bothnetlify.toml(SUR-227, Netlify) andpublic/_headers(SUR-254, Cloudflare Pages). The two files coexist during the Netlify → Cloudflare Pages transition..nvmrcpins Node 22 for Cloudflare Pages parity with Netlify (SUR-254).
Environment
PUBLIC_*env vars (Astro convention) for any build-time secrets. The waitlist Edge Function URL is the only runtime dependency — it is thesurfc/Supabase project URL.
Confirmed vs. assumption vs. unknown
- Confirmed:
- React 18 + Vite client, Dexie local DB, Supabase backend, Anthropic AI calls via
anthropic-proxyEdge Function (surfc/). - Offline-first queueing via
useAuthand Dexie outbox, plus PWA bundling through VitePWA. - Vitest test harness given
package.jsonscripts andvite.config.jstest config. - Client-side PII detection (
src/safety/) and server-side Azure AI Content Safety guardrails (guardrail.ts) active (SUR-242, 2026-05-01). Fail-open semantics confirmed. - Astro 6 static site for marketing (surfc-web/), deployed on Netlify; Playwright + Lighthouse CI in place.
- Self-hosted fonts via
@fontsource/*, font-display optional (SUR-227). public/_headers+.nvmrcinsurfc-web/for Cloudflare Pages cutover (SUR-254, 2026-04-27).- Android TWA credential sharing enabled via
public/.well-known/assetlinks.json(delegate_permission/common.get_login_creds, 2026-04-30). - Founder blog at
surfc-web/(/blog/) using Astro content collections, MDX, RSS, paginated index, and reading-time (SUR-256, 2026-04-30). No Cloudflare adapter; output is flat static. - In-app help rendering using
react-markdown+remark-gfm+remark-directive(SUR-303, 2026-05-04).src/help/manifest.jsbundlesdocs/getting-started/*.mdat Vite build time. Routes/helpand/help/:slugare public (no auth required). AppGates,ShellNavigation,NoteActionOverlay,IdeaActionOverlay,UnsyncedChangesModalextracted fromApp.jsx;useUserProfileanduseMediaQueryhooks added (SUR-303, 2026-05-04).
- React 18 + Vite client, Dexie local DB, Supabase backend, Anthropic AI calls via
- Assumption:
- Future ingest adapters (Readwise/Kindle) will reuse the existing
ingestinterface without backend changes. - Both Netlify deploys (surfc/ and surfc-web/) remain manual or handled by external CI; no workflow files exist.
- Azure Content Safety threshold (severity ≥5) validated in SUR-242 spike against Surfc content; may need re-tuning as usage grows.
- Future ingest adapters (Readwise/Kindle) will reuse the existing
- Unknown:
- No evidence of mobile wrappers or desktop builds beyond the PWA.
- Server-side monitoring/logging stack is not referenced; Supabase defaults are assumed.
- Whether the Astro build will ever need client islands (JS hydration) is undefined.
surfc-web/Playwright test suite coverage for blog routes — tests exist (tests/) but CI config specifics are not committed.