Skip to content

Deployment Architecture

Deployment Architecture

CHANGE SUMMARY

  • Updated: Captured the new managed Anthropic proxy deployment surface — Supabase Edge Function plus service-role + Anthropic secrets — and how it fits into the release workflow (supabase/functions/anthropic-proxy/index.ts).
  • Updated: Environment configuration now calls out the Supabase Functions secrets required for managed usage.
  • Updated (2026-04-23): Added surfc-web/ Astro marketing site deployment topology, CI pipeline (Lighthouse + Playwright + Lychee), and font-caching details reflecting SUR-218/SUR-227.
  • Updated (2026-04-26, SUR-261): Added email-OTP sign-in rollout checklist and the new supabase/email-templates/ source-of-truth directory for the branded magic-link template.
  • Updated (2026-04-27, SUR-254): surfc-web/ now has a public/_headers file for Cloudflare Pages cache rules and .nvmrc pinning Node 22 — both in preparation for a Netlify → Cloudflare Pages cutover. netlify.toml intentionally remains until stability is confirmed.
  • Updated (2026-04-30): public/.well-known/assetlinks.json now declares delegate_permission/common.get_login_creds in addition to URL handling, enabling Google Smart Lock credential sharing for the Android TWA. A second SHA-256 fingerprint was also added.
  • Updated (2026-05-01, SUR-242): Azure AI Content Safety secrets added to Edge Function prerequisites. Managed-AI change checklist updated.
  • Updated (2026-05-01, SUR-256): surfc-web/ now serves a founder blog at /blog/ — Astro content collections, MDX, RSS. No @astrojs/cloudflare adapter (static output). surfc-web/ CI section updated.
  • Updated (2026-05-02, SUR-237): Migration 0014 adds a server-side DEFAULT ((extract(epoch from now()) * 1000)::bigint) to wrapped_key_blobs.created_at and introduces the select_fresh_transfer_blob(p_user_id, p_max_age_ms) SECURITY DEFINER RPC as the authoritative TTL gate for transfer-v1 blobs. redeemDeviceTransfer now calls this RPC instead of performing a client-side age check. Client-side TRANSFER_MAX_AGE_MS / TRANSFER_MAX_FUTURE_SKEW_MS constants removed from deviceTransfer.js; TRANSFER_SANITY_FUTURE_SKEW_MS (5 min) retained as defense-in-depth. Migration 0014 must be applied to the Supabase project before deploying clients that call redeemDeviceTransfer.
  • Updated (2026-05-04, SUR-303 refactor/sur-303-extract): supabase/functions/anthropic-proxy/prompts.ts extracted from index.ts — system prompts now live in a dedicated module imported by both the Edge Function and the SUR-300 eval harness. src/constants.js gains a header comment documenting its Vite-neutral constraint (required for the Deno cross-tree import).
  • Updated (2026-05-04, SUR-303): /help and /help/:slug routes added to the React app — served in-app (public, no auth). /policies/:kind now rendered in-app via PolicyPage + Termly embed rather than pure 301 redirect. In-app help articles (docs/getting-started/*.md) are bundled at Vite build time.
  • Updated (2026-06-20, SUR-295): surfc-intranet/ adopts the @astrojs/cloudflare adapter so the admin surface can render on demand. Astro’s default output stays static — the Starlight wiki remains fully prerendered — and only src/pages/admin/* opts out via export const prerender = false. AdminLayout.astro reads the operator identity from the Cf-Access-Authenticated-User-Email request header (display only; the trust boundary is Cloudflare Access, which covers the production domain and the CF Pages preview hostnames), with a dev@surfc.local fallback for local astro dev. The admin overview now mounts the first React island (src/components/AdminHome.jsx, client:load) rendering KPI tile shells; data wiring follows in SUR-294.
  • Updated (2026-05-31, SUR-255): the React PWA (surfc/, app.surfc.app) cut over from Netlify to Cloudflare Pages. netlify.toml removed; redirects (policies 301, change-password 302, SUR-480 help-slug 301s, SPA /* → /index.html fallback) and headers (manifest MIME, /assets/* immutable) now live in public/_redirects + public/_headers. .nvmrc pins Node 22. Auto-deploys on push to main via Cloudflare Pages git integration. Cloudflare Pages has no force flag — Netlify’s 301! is invalid and silently dropped, so the policies bounce uses a plain 301 that wins by rule order.
  • Updated (2026-06-23, SUR-537): PWA service-worker registration + precache boundary corrected. Two vite.config.js changes were required for the SW to register and install at all on a fresh/cleared device (it had been silently broken since the Rolldown bundler migration; an already-registered SW masked it until users cleared data). (1) Registration moved into app codesrc/pwa/registerServiceWorker.js, called from main.jsx, with injectRegister: false. Rolldown silently drops vite-plugin-pwa’s registerSW.js (a Rollup generateBundle hook it ignores) AND its inline fallback is bare (register-only, no update polling or reload-on-deploy), so registration never ran (no offline; the SUR-537 Web Share Target POST 405’d) and, once fixed inline, deploys still didn’t propagate (users stuck on stale precached code). Owning it in the bundle (which Rolldown handles) adds auto-update: reload once when a new worker activates over an existing controller (a deploy on a running app, never the first install), poll registration.update() hourly for long-lived TWA sessions, and defer the reload while an unsaved note draft is open (src/pwa/reloadGuard.js, fed by noteForm.hasDraftContent) so a mid-edit deploy can’t discard the draft. (2) The Workbox precache now excludes **/policies/** (workbox.globIgnores): policies/*.html are still emitted to dist, but the /policies/* → https://surfc.app/policies/* 301 (SUR-218) is cross-origin — precaching followed it and Workbox’s copyResponse threw cross-origin-copy-response, which fails the entire SW install. Boundary constraint going forward: any precached path that a _redirects rule bounces CROSS-ORIGIN will fail the service-worker install — exclude it from the precache (globIgnores) or stop emitting the file.
  • Updated (2026-06-28, SUR-695): braird.app added to the functional Edge Function CORS / redirect allow-lists for the dual-domain rebrand window. Four functions carry server-side allow-lists that gate real traffic (distinct from Supabase auth redirect URLs, owned by SUR-671, and cosmetic string purges, SUR-680): create-checkout-session + create-billing-portal-session match a trailing-slash ALLOWED_REDIRECT_PREFIXES array via startsWith; contact-us + fetch-link-metadata match a bare-origin ALLOWED_ORIGINS Set via exact has(). The two conventions are NOT interchangeable — a trailing slash in the Set (or its absence in the prefix array) silently breaks the match. Every entry is additive: app.braird.app (and the apex braird.app for the checkout marketing-pricing redirect, SUR-86) join the existing surfc origins; both domains stay live until surfc is dropped at sunset (SUR-683). The billing functions also send Access-Control-Allow-Origin: '*', so only their redirect arrays matter, not CORS. fetch-link-metadata is the SSRF boundary the native share path hits (SUR-662); the Android TWA sends the app.braird.app web origin (now covered), and per ADR 0001 there is no Capacitor client, so no capacitor:// origin is needed (future fully-native SDK clients send no Origin and bypass CORS, gated by JWT + the SSRF resolveHost boundary). These four functions are NOT auto-deployed — merge auto-deploys the frontend only; each requires a manual supabase functions deploy <name> (on Windows add --use-api). Prod verification of braird traffic depends on SUR-671 landing (braird auth-redirect URLs) — without it braird users cannot authenticate and never reach these JWT-gated functions. CI (edge-functions-test.yml) was extended to type-check and run the checkout/portal/fetch-link test dirs, which previously ran only contact-us of the four. (surfc#330.)

This document covers both sibling repos. Descriptions are based on repo evidence only.

Overview

graph TD
Dev[Dev machine\nnpm run dev] --> Build[Vite build\nnpm run build]
Build --> Dist[dist/ static bundle]
Dist --> CFPages[Cloudflare Pages — app.surfc.app\npublic/_redirects + public/_headers]
CFPages --> Browser[PWA client]
Browser --> SupabaseEnv[Supabase project\nURL+anon key]
Browser --> EdgeFN[Supabase Edge Functions\nanthropic-proxy]
Browser --> AnthropicAPI[Anthropic HTTPS (BYOK)]
SupabaseEnv --> StorageBucket[note-images bucket]
EdgeFN --> SupabaseEnv
EdgeFN --> AnthropicAPI

Build and bundling

  • npm run build executes vite build per package.json and outputs dist/. This is the Cloudflare Pages build command (build output directory dist/).
  • Vite config applies @vitejs/plugin-react and VitePWA, enabling automatic service-worker generation, manifest injection, and Workbox runtime caching for fonts/assets (vite.config.js).
  • npm run dev starts the Vite dev server with React Fast Refresh; npm run preview serves the built assets locally. Testing commands (npm run test*) run Vitest with the config embedded in vite.config.js.

Environment configuration

  • .env must define VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY; Vite inlines these at build time and the client reads them via import.meta.env (src/supabase.js).
  • Users can supply an Anthropic API key in-app for BYOK. Managed mode requires Supabase Edge Function secrets: ANTHROPIC_API_KEY, SUPABASE_SERVICE_ROLE_KEY, and the standard SUPABASE_URL/SUPABASE_ANON_KEY env vars injected into the function environment (supabase/functions/anthropic-proxy/index.ts).
  • Optional environment-specific assets (favicon, manifest icons) are referenced via VitePWA’s includeAssets array and public/ folder.

Hosting topology

  • Cloudflare Pages serves the static bundle from dist/. SPA routing (catch-all rewrite to /index.html), the /policies/* → marketing 301, the /.well-known/change-password 302, the SUR-480 help-slug 301s, the manifest MIME header, and the /assets/* immutable cache are all configured in public/_redirects + public/_headers (SUR-255; netlify.toml removed).
  • The service worker is registered by app code (src/pwa/registerServiceWorker.js, called from main.jsx; injectRegister: false, VitePWA registerType: 'autoUpdate'), precaching compiled JS/CSS/HTML/assets and caching Google Fonts per Workbox config (vite.config.js). App-owned registration is required because the Rolldown bundler drops the separate registerSW.js and its inline fallback has no update/reload logic (SUR-537); owning it adds auto-update (reload-on-new-worker, hourly update() poll) with the reload deferred while an unsaved note draft is open. The precache excludes the lazy guardrail/embedding workers and **/policies/** — the latter because /policies/* 301-redirects cross-origin to the marketing site, which would otherwise fail the SW install (SUR-537; see CHANGE SUMMARY).
  • There is no server-side rendering tier; all business logic executes in the browser.

Supabase prerequisites

  • Provision a Supabase project, enable the uuid-ossp extension, and apply the versioned SQL files under supabase/migrations/*.sql (via the Supabase SQL editor or CLI) to create tables, policies, the ai_usage ledger, and the note-images bucket.
  • Configure the bucket as non-public; supabase/migrations/0001_initial_schema.sql installs the users manage own images policy tying folder prefixes to auth.uid().
  • Deploy the supabase/functions/anthropic-proxy Edge Function (Supabase CLI or dashboard) and set its secrets: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY, POSTHOG_API_KEY (optional, PostHog telemetry), AZURE_CONTENT_SAFETY_ENDPOINT, AZURE_CONTENT_SAFETY_KEY, and AZURE_CONTENT_SAFETY_API_VERSION (optional, defaults to 2024-09-01). Missing Azure secrets cause the guardrails to fail-open (not fail-closed) — managed calls still succeed, but injection/harm protection is inactive.
  • After running migrations, execute npm run check:schema so scripts/check-schema.js can verify the live database matches scripts/schema-contract.js before deploying dependent clients.
  • Ensure the Supabase project URL and anon key from Settings -> API are added to .env prior to building/deploying.

Release & verification workflow

  • Recommended local workflow:
    1. npm install (first run).
    2. npm run test (Vitest suites).
    3. npm run build (validate bundling).
    4. Push to main — Cloudflare Pages builds and deploys automatically.
  • There is no CI/CD configuration in the repo; deployments are assumed to be manual or handled outside this codebase.
  • Supabase schema changes must still be applied manually (run the migrations, then npm run check:schema) before deploying a client that depends on them; no automation enforces this order.

Managed-AI change checklist

Run this checklist whenever a PR touches supabase/functions/anthropic-proxy/ or supabase/migrations/.

Prompt changes: If you edit supabase/functions/anthropic-proxy/prompts.ts, the SUR-300 eval harness imports the same file — run the eval suite after editing to confirm the new prompt does not regress transcription or discovery quality before deploying.

  1. Migrations first — apply every new supabase/migrations/*.sql file in the Supabase SQL editor before deploying the Edge Function or a new client build. The Edge Function calls upsert_ai_usage via .rpc(); if the function doesn’t exist in the live database the managed-AI path throws 500 for all users.
  2. Schema contract — run npm run check:schema after applying migrations and confirm zero failures before proceeding.
  3. Secrets in sync — if the PR adds or renames an env var read by the Edge Function, set the updated secret in the Supabase dashboard (Project → Edge Functions → Manage secrets) before deploying the function. Required secrets: ANTHROPIC_API_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_URL, SUPABASE_ANON_KEY. Optional: POSTHOG_API_KEY, AZURE_CONTENT_SAFETY_ENDPOINT, AZURE_CONTENT_SAFETY_KEY, AZURE_CONTENT_SAFETY_API_VERSION. A missing Azure secret causes guardrails to fail-open (not a hard outage), but should still be set. A missing Anthropic or service-role secret silently resolves to an empty string and causes all managed calls to fail with 401/Anthropic auth errors.
  4. Deploy the Edge Functionsupabase functions deploy anthropic-proxy. Verify the deploy succeeded in the Supabase dashboard before merging to main.
  5. Smoke-test managed mode — with no BYOK key configured in the app, trigger one transcription and one idea discovery and confirm both complete without a 500/401 error. Confirm the response does not contain _failOpen: true if Azure secrets are expected to be live.
  6. Deploy the client — merge to main (Cloudflare Pages auto-deploys) only after steps 1–5 are clean. This order ensures no client ever calls a proxy that depends on a migration or secret that isn’t yet live.

Email-OTP / AuthScreen change checklist (SUR-261)

Run this checklist whenever a PR touches src/AuthScreen.jsx, requestEmailOtp / verifyEmailOtp in src/supabase.js, or supabase/email-templates/:

  1. Auth provider settings — confirm in Supabase → Authentication → Providers:
    • Email provider: enabled (required for signInWithOtp to dispatch the OTP / magic link).
    • In Authentication → Settings: “Enable email signups”: disabled. Sign-up is admin-issued only; shouldCreateUser:false in the client is defence-in-depth, not the primary gate.
    • Google provider: enabled (unchanged).
  2. Pre-flight waitlist audit — query for approved rows that never completed signup:
    SELECT id, email, name, status FROM public.waitlist_requests
    WHERE status='approved' AND user_id IS NULL;
    For each row, send a Supabase Auth invite (dashboard → Authentication → Users → “Invite user”, or POST /auth/v1/invite with the service-role key for batches). The handle_new_auth_user trigger (0020:43-72, SUR-362) creates the user_profiles row on first signin so the user has a quota immediately; back-linking waitlist_requests.user_id is no longer automatic — the approve-waitlist Edge Function still upserts approval-derived columns when the row arrives.
  3. Branded email template — paste the contents of supabase/email-templates/magic-link.html into Supabase → Authentication → Email Templates → Magic Link. The template branches on {{ if .Token }} so the same template body covers the 6-digit code (TWA / standalone PWA) and the magic link (desktop) paths. Set the subject to Your Surfc sign-in code. The repo file is the source of truth — the README at supabase/email-templates/README.md documents the upload procedure.
  4. Smoke test the live flow with a real email:
    • Approved-user email on desktop → magic link arrives → click → signed in.
    • Approved-user email in TWA / installed PWA → 6-digit code arrives → enter code → signed in.
    • Unknown email → friendly waitlist CTA renders (Signups not allowed for otp mapping); confirm in the Supabase dashboard that no new auth.users row was created for that address.
  5. Deploy the client — merge to main (Cloudflare Pages auto-deploys) only after steps 1–4 are clean.

surfc-web/ — Astro marketing site (surfc.app)

Introduced SUR-218 (2026-04-22). Deployed separately from the React PWA.

Build and bundling

  • npm run build executes astro build from surfc-web/package.json and outputs surfc-web/dist/.
  • Astro performs static site generation with zero client JS by default. Fonts are bundled from @fontsource/* npm packages with font-display: optional (no Google Fonts CDN) to support long-lived cache headers (SUR-227).
  • npm run dev starts astro dev; npm run preview serves the built site locally.

Hosting topology

  • Netlify currently serves surfc-web/dist/ at surfc.app. SPA routing is not needed (static pages; output: 'static' in astro.config.mjs; no @astrojs/cloudflare adapter).
  • Long-lived Cache-Control: max-age=31536000, immutable headers are applied to /_astro/*, favicons, and OG assets via both netlify.toml (SUR-227, Netlify-specific) and public/_headers (SUR-254, Cloudflare Pages-compatible). Both files intentionally coexist; netlify.toml will be removed after the Cloudflare Pages cutover is confirmed stable.
  • .nvmrc pins Node 22 so Cloudflare Pages uses the same Node version as the current Netlify build (SUR-254).
  • The www→apex 301 from netlify.toml does not move into _headers or _redirects (Cloudflare’s _redirects is path-only); it will become a Cloudflare zone-level Redirect Rule at cutover time.
  • Runtime dependencies: the waitlist-signup Edge Function (from surfc/), and the blog RSS feed at /rss.xml (static, no runtime dependency).
  • Blog posts at src/content/blog/ are baked into the static build at dist/blog/<slug>/index.html. No CMS integration — posts are MDX files checked into the repo. Adding a post requires a new commit and a Netlify/Cloudflare Pages rebuild.

CI pipeline (surfc-web)

  • Lighthouse CI (lighthouserc.cjs) — runs on every PR via GitHub Actions. Category thresholds only; recommended preset is disabled. Warns on performance/a11y regressions without blocking merge.
  • Playwright (playwright.config.ts) — E2E smoke suite (tests/). Termly consent script is blocked in fixtures to avoid network flakiness. The waitlist Edge Function is mocked via Playwright route intercept.
  • Lychee link check — validates internal and external links on every PR using --root-dir (absolute path mode).

Environment

  • No build-time secrets in surfc-web/; the Supabase Edge Function URL is the only runtime dependency, hardcoded or injected at deploy time.
  • PUBLIC_* env var convention (Astro) is available for any future build-time values.

Confirmed / Assumption / Unknown

  • Confirmed:
    • Static build/deploy workflow via Vite + Netlify + Supabase environment variables (package.json, vite.config.js, netlify.toml).
    • Service worker + manifest configuration handled by VitePWA with auto-update strategy.
    • Supabase schema requirements (including ai_usage) in supabase/migrations/*.sql; managed AI requires the Edge Function.
    • Astro 6 marketing site deployed on Netlify (surfc.app); Playwright + Lighthouse CI + Lychee link-check are active for surfc-web/ (SUR-218/SUR-227).
    • Self-hosted fonts with long-lived cache headers on surfc-web/ (SUR-227).
    • public/_headers + .nvmrc added to surfc-web/ for a Cloudflare Pages cutover (SUR-254, 2026-04-27). Netlify remains the live host until cutover is confirmed.
    • public/.well-known/assetlinks.json declares URL handling + credential sharing for the Android TWA (com.surfc.app), with two registered fingerprints (release + debug/test keystores).
  • Assumption:
    • Both Netlify deploys (surfc/ and surfc-web/) are manual or handled by external CI; no workflow files are committed to either repo.
    • Supabase Function secrets are configured manually in the Supabase dashboard or CLI; no IaC defines them.
    • The surfc-web/ Cloudflare Pages cutover (SUR-254) will be completed in a follow-up cleanup PR that removes netlify.toml; the www→apex redirect will be handled as a Cloudflare zone-level rule.
  • Unknown:
    • Enforcement of running migrations plus npm run check:schema and redeploying the Edge Function is undefined; no pipeline guarantees the order.
    • Monitoring/alerting around deployment health (Netlify build + Edge Function metrics) is not described.
    • Whether surfc-web/ Playwright tests run in CI against a preview deploy or a local astro preview is not defined in committed config.
    • Exact timeline for Netlify → Cloudflare Pages cutover for surfc-web/ is not committed.