Skip to content

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 at supabase/email-templates/magic-link.html.
  • Updated (2026-04-27, SUR-254): surfc-web/ static assets section updated — public/_headers added for Cloudflare Pages, .nvmrc pinning Node 22.
  • Updated (2026-04-30): public/.well-known/assetlinks.json now 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.ts in anthropic-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/cloudflare adapter intentionally absent (static output).
  • Updated (2026-05-04, SUR-303 refactor/sur-303-extract): anthropic-proxy/prompts.ts extracted as single source of truth for managed-AI system prompts; GREAT_IDEAS now sourced from src/constants.js via Deno cross-tree import (constants.js must remain Vite-neutral). __tests__/prompts.test.ts added. src/constants.js header comment added documenting the Vite-neutral constraint.
  • Updated (2026-05-02, SUR-237): Migration 0014 adds server-stamped DEFAULT to wrapped_key_blobs.created_at and introduces select_fresh_transfer_blob SECURITY DEFINER RPC. upsertWrappedKeyBlob drops createdAt for new inserts; redeemDeviceTransfer uses the RPC for freshness checks. Client-side TRANSFER_MAX_AGE_MS / TRANSFER_MAX_FUTURE_SKEW_MS constants removed; TRANSFER_SANITY_FUTURE_SKEW_MS (5 min) kept as defense-in-depth. Test device-transfer-server-time.test.js replaces device-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. Added src/help/ infrastructure documentation. AppGates, ShellNavigation, overlay components extracted from App.jsx. New hooks: useMediaQuery, useUserProfile.
  • Updated (2026-05-03, SUR-233 follow-up): fetchDeviceList in useKeyManagement.js memoized with useCallback([session?.user?.id]). Regression test src/test/useKeyManagement-stability.test.jsx added.
  • Updated (2026-05-02, surfc-web): Fourth blog post published — surfc-architecture.mdx with surfc-detailed-architecture.svg architecture 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 under src/components/ and wires gestures via src/hooks/useLongPress.js.
  • Styling is global via tokens.css and styles.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, and outbox tables with six schema versions and upgrade steps (src/db.js).
  • useAuth hydrates Dexie via loadAll, exposes books/notes/customIdeas/apiKey, tracks connectivity, and coordinates sync (src/hooks/useAuth.js).
  • useNoteForm, useSettings, and useNoteActions mutate Dexie entities, manage captured images, export/import archives, and enqueue cloud writes via the shared cloudWrite helper.
  • src/test/sync.test.js uses fake-indexeddb to validate merge semantics; src/test/outbox.test.js covers the queue collapse logic.

Cloud sync & storage

  • Supabase JS client handles auth, Postgres tables (books, notes, custom_ideas), and storage bucket note-images (src/supabase.js).
  • SQL schema and RLS policies are defined by the versioned migrations under supabase/migrations/*.sql plus the contract in scripts/schema-contract.js; npm run check:schema calls scripts/check-schema.js to verify the live database matches.
  • useAuth.cloudWrite attempts upserts immediately, queuing failures in Dexie outbox; syncFromCloud flushes the queue, fetches Supabase tables via fetchAllCloud, merges them locally (mergeCloudRecords), and backfills missing images via downloadImage (src/hooks/useAuth.js, src/db.js).
  • .env supplies VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY, which Vite injects into the client bundle (src/supabase.js).

Authentication

  • src/AuthScreen.jsx is 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 shared src/components/HomeHero.jsx so AuthScreen and HomeScreen stay visually identical.
  • Primary CTA: Google OAuth. signInWithGoogle() in src/supabase.js calls supabase.auth.signInWithOAuth({ provider: 'google' }) and redirects back to window.location.origin.
  • Secondary CTA: “Use a different account” opens src/components/BottomSheet.jsx containing the email-OTP sign-in flow:
    • requestEmailOtp(email)signInWithOtp with shouldCreateUser:false. Picks 6-digit code on TWA / standalone PWA (no emailRedirectTo) vs magic link on desktop (emailRedirectTo: window.location.origin). Platform decided by isStandaloneOrTwa() in src/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_requests row by email — back-filled by the match_waitlist_on_signup trigger in supabase/migrations/0007_waitlist_requests.sql) or via admin-issued Supabase Auth invite. Email-OTP rejects unknown emails at the platform level (shouldCreateUser:false) and AuthScreen maps Supabase’s Signups not allowed for otp error to a friendly waitlist CTA pointing at https://surfc.app/waitlist.
  • Email branding: supabase/email-templates/magic-link.html is 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/messages powers both transcription and tagging through callTranscribeImage and callDiscoverIdeas. All calls go through the managed anthropic-proxy Edge Function (BYOK sunset, SUR-91). Managed calls post { action, payload, mimeType?, customIdeas } plus the Supabase session token to invokeAnthropicProxy (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), and buildDiscoverSystem(customIdeas) (Syntopicon assistant builder). GREAT_IDEAS is imported from src/constants.js via 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.js compresses camera captures (src/utils.js), calls callTranscribeImage, and hands normalized notes back to useNoteForm. Manual notes use src/ingest/manualAdapter.js.
  • Tag discovery is triggered from useNoteForm.discoverIdeas and useNoteActions.rediscoverIdeas, with results persisted into Dexie notes.tags.

In-app help rendering (SUR-303, 2026-05-04)

  • react-markdown ^10.1.0 — markdown-to-React renderer used by HelpArticleBody. Configured with custom components to 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, :::danger container directives in the help article markdown.
  • src/help/manifest.js — reads docs/getting-started/*.md at build time via import.meta.glob({ query: '?raw', eager: true }); exposes ordered articles array, getArticle(slug), and FORWARD_REFERENCES for link-validity checks.
  • src/help/calloutPlugin.js — remark plugin that transforms directive container nodes into callout AST nodes consumed by HelpArticleBody’s components map.
  • Routes /help (public) and /help/:slug (public) are defined in App.jsx outside AppGates — accessible without authentication.

Client-side safety

  • src/safety/index.jsrunSafetyChecks(text) entry point. Currently runs checkStructuredPii and returns { matches: PiiMatch[], blocked: false }. blocked is 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 consume PiiMatch[] and present the BottomSheet review UI.
  • Stub seams checkPromptInjection() and checkNerPii() 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) and moderate(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 (default 2024-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.ts as five stages (input shield → Anthropic call → transcription-post spotlight → output moderation → usage record). See SYSTEM_ARCHITECTURE.md → Safety guardrail pipeline for 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, serves dist/, 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.json scripts, src/test/*.test.js[x]).
  • CSS tokens/variables centralize design primitives in src/tokens.css, complemented by styles.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 .astro files 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) with font-display: optional to 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 to supabase/functions/waitlist-signup/ Edge Function (deployed from surfc/). Honeypot field named hp_trap (renamed from hp_company in SUR-218 fix to defeat autofill).
  • src/components/{Nav,Hero,ClosingCta,Faq,Footer,…}.astro — landing sections; CTAs tagged with data-cta attributes 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 for blog; uses glob loader 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_SIZE from src/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 — injects minutesRead into post frontmatter via remark.
  • src/utils/blog.js — shared helpers: isPublished filter, sortByDateDesc, PAGE_SIZE.
  • astro.config.mjs updated: added mdx() integration, rehype-slug, rehype-autolink-headings (append mode), remarkReadingTime. trailingSlash: 'always' enforced. No @astrojs/cloudflare adapter — static output is flat dist/ which Cloudflare Pages publishes directly. The adapter was briefly added in SUR-256 then removed because its dist/{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 (no recommended preset).
  • 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-dir mode).

Static assets

  • public/ — images, favicons, OG assets. Long-lived Cache-Control: max-age=31536000, immutable headers for /_astro/* and static assets are set in both netlify.toml (SUR-227, Netlify) and public/_headers (SUR-254, Cloudflare Pages). The two files coexist during the Netlify → Cloudflare Pages transition.
  • .nvmrc pins 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 the surfc/ Supabase project URL.

Confirmed vs. assumption vs. unknown

  • Confirmed:
    • React 18 + Vite client, Dexie local DB, Supabase backend, Anthropic AI calls via anthropic-proxy Edge Function (surfc/).
    • Offline-first queueing via useAuth and Dexie outbox, plus PWA bundling through VitePWA.
    • Vitest test harness given package.json scripts and vite.config.js test 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 + .nvmrc in surfc-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.js bundles docs/getting-started/*.md at Vite build time. Routes /help and /help/:slug are public (no auth required).
    • AppGates, ShellNavigation, NoteActionOverlay, IdeaActionOverlay, UnsyncedChangesModal extracted from App.jsx; useUserProfile and useMediaQuery hooks added (SUR-303, 2026-05-04).
  • Assumption:
    • Future ingest adapters (Readwise/Kindle) will reuse the existing ingest interface 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.
  • 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.