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 apublic/_headersfile for Cloudflare Pages cache rules and.nvmrcpinning Node 22 — both in preparation for a Netlify → Cloudflare Pages cutover.netlify.tomlintentionally remains until stability is confirmed.- Updated (2026-04-30):
public/.well-known/assetlinks.jsonnow declaresdelegate_permission/common.get_login_credsin 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/cloudflareadapter (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)towrapped_key_blobs.created_atand introduces theselect_fresh_transfer_blob(p_user_id, p_max_age_ms)SECURITY DEFINER RPC as the authoritative TTL gate for transfer-v1 blobs.redeemDeviceTransfernow calls this RPC instead of performing a client-side age check. Client-sideTRANSFER_MAX_AGE_MS/TRANSFER_MAX_FUTURE_SKEW_MSconstants removed fromdeviceTransfer.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 callredeemDeviceTransfer.- Updated (2026-05-04, SUR-303 refactor/sur-303-extract):
supabase/functions/anthropic-proxy/prompts.tsextracted fromindex.ts— system prompts now live in a dedicated module imported by both the Edge Function and the SUR-300 eval harness.src/constants.jsgains a header comment documenting its Vite-neutral constraint (required for the Deno cross-tree import).- Updated (2026-05-04, SUR-303):
/helpand/help/:slugroutes added to the React app — served in-app (public, no auth)./policies/:kindnow rendered in-app viaPolicyPage+ 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/cloudflareadapter so the admin surface can render on demand. Astro’s default output stays static — the Starlight wiki remains fully prerendered — and onlysrc/pages/admin/*opts out viaexport const prerender = false.AdminLayout.astroreads the operator identity from theCf-Access-Authenticated-User-Emailrequest header (display only; the trust boundary is Cloudflare Access, which covers the production domain and the CF Pages preview hostnames), with adev@surfc.localfallback for localastro 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.tomlremoved; redirects (policies 301, change-password 302, SUR-480 help-slug 301s, SPA/* → /index.htmlfallback) and headers (manifest MIME,/assets/*immutable) now live inpublic/_redirects+public/_headers..nvmrcpins Node 22. Auto-deploys on push tomainvia Cloudflare Pages git integration. Cloudflare Pages has noforceflag — Netlify’s301!is invalid and silently dropped, so the policies bounce uses a plain301that wins by rule order.- Updated (2026-06-23, SUR-537): PWA service-worker registration + precache boundary corrected. Two
vite.config.jschanges 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 code —src/pwa/registerServiceWorker.js, called frommain.jsx, withinjectRegister: false. Rolldown silently drops vite-plugin-pwa’sregisterSW.js(a RollupgenerateBundlehook it ignores) AND itsinlinefallback 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), pollregistration.update()hourly for long-lived TWA sessions, and defer the reload while an unsaved note draft is open (src/pwa/reloadGuard.js, fed bynoteForm.hasDraftContent) so a mid-edit deploy can’t discard the draft. (2) The Workbox precache now excludes**/policies/**(workbox.globIgnores):policies/*.htmlare still emitted todist, but the/policies/* → https://surfc.app/policies/*301 (SUR-218) is cross-origin — precaching followed it and Workbox’scopyResponsethrewcross-origin-copy-response, which fails the entire SW install. Boundary constraint going forward: any precached path that a_redirectsrule 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-sessionmatch a trailing-slashALLOWED_REDIRECT_PREFIXESarray viastartsWith;contact-us+fetch-link-metadatamatch a bare-originALLOWED_ORIGINSSet via exacthas(). 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 apexbraird.appfor 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 sendAccess-Control-Allow-Origin: '*', so only their redirect arrays matter, not CORS.fetch-link-metadatais the SSRF boundary the native share path hits (SUR-662); the Android TWA sends theapp.braird.appweb origin (now covered), and per ADR 0001 there is no Capacitor client, so nocapacitor://origin is needed (future fully-native SDK clients send no Origin and bypass CORS, gated by JWT + the SSRFresolveHostboundary). These four functions are NOT auto-deployed — merge auto-deploys the frontend only; each requires a manualsupabase 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 onlycontact-usof 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 --> AnthropicAPIBuild and bundling
npm run buildexecutesvite buildperpackage.jsonand outputsdist/. This is the Cloudflare Pages build command (build output directorydist/).- Vite config applies
@vitejs/plugin-reactandVitePWA, enabling automatic service-worker generation, manifest injection, and Workbox runtime caching for fonts/assets (vite.config.js). npm run devstarts the Vite dev server with React Fast Refresh;npm run previewserves the built assets locally. Testing commands (npm run test*) run Vitest with the config embedded invite.config.js.
Environment configuration
.envmust defineVITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEY; Vite inlines these at build time and the client reads them viaimport.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 standardSUPABASE_URL/SUPABASE_ANON_KEYenv vars injected into the function environment (supabase/functions/anthropic-proxy/index.ts). - Optional environment-specific assets (favicon, manifest icons) are referenced via VitePWA’s
includeAssetsarray andpublic/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-password302, the SUR-480 help-slug 301s, the manifest MIME header, and the/assets/*immutable cache are all configured inpublic/_redirects+public/_headers(SUR-255;netlify.tomlremoved). - The service worker is registered by app code (
src/pwa/registerServiceWorker.js, called frommain.jsx;injectRegister: false, VitePWAregisterType: '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 separateregisterSW.jsand its inline fallback has no update/reload logic (SUR-537); owning it adds auto-update (reload-on-new-worker, hourlyupdate()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-osspextension, and apply the versioned SQL files undersupabase/migrations/*.sql(via the Supabase SQL editor or CLI) to create tables, policies, the ai_usage ledger, and thenote-imagesbucket. - Configure the bucket as non-public;
supabase/migrations/0001_initial_schema.sqlinstalls theusers manage own imagespolicy tying folder prefixes toauth.uid(). - Deploy the
supabase/functions/anthropic-proxyEdge 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, andAZURE_CONTENT_SAFETY_API_VERSION(optional, defaults to2024-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:schemasoscripts/check-schema.jscan verify the live database matchesscripts/schema-contract.jsbefore deploying dependent clients. - Ensure the Supabase project URL and anon key from Settings -> API are added to
.envprior to building/deploying.
Release & verification workflow
- Recommended local workflow:
npm install(first run).npm run test(Vitest suites).npm run build(validate bundling).- 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.
- Migrations first — apply every new
supabase/migrations/*.sqlfile in the Supabase SQL editor before deploying the Edge Function or a new client build. The Edge Function callsupsert_ai_usagevia.rpc(); if the function doesn’t exist in the live database the managed-AI path throws 500 for all users. - Schema contract — run
npm run check:schemaafter applying migrations and confirm zero failures before proceeding. - 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. - Deploy the Edge Function —
supabase functions deploy anthropic-proxy. Verify the deploy succeeded in the Supabase dashboard before merging tomain. - 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: trueif Azure secrets are expected to be live. - 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/:
- Auth provider settings — confirm in Supabase → Authentication → Providers:
- Email provider: enabled (required for
signInWithOtpto dispatch the OTP / magic link). - In Authentication → Settings: “Enable email signups”: disabled. Sign-up is admin-issued only;
shouldCreateUser:falsein the client is defence-in-depth, not the primary gate. - Google provider: enabled (unchanged).
- Email provider: enabled (required for
- Pre-flight waitlist audit — query for approved rows that never completed signup:
For each row, send a Supabase Auth invite (dashboard → Authentication → Users → “Invite user”, orSELECT id, email, name, status FROM public.waitlist_requestsWHERE status='approved' AND user_id IS NULL;
POST /auth/v1/invitewith the service-role key for batches). Thehandle_new_auth_usertrigger (0020:43-72, SUR-362) creates theuser_profilesrow on first signin so the user has a quota immediately; back-linkingwaitlist_requests.user_idis no longer automatic — theapprove-waitlistEdge Function still upserts approval-derived columns when the row arrives. - Branded email template — paste the contents of
supabase/email-templates/magic-link.htmlinto 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 toYour Surfc sign-in code. The repo file is the source of truth — the README atsupabase/email-templates/README.mddocuments the upload procedure. - 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 otpmapping); confirm in the Supabase dashboard that no newauth.usersrow was created for that address.
- 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 buildexecutesastro buildfromsurfc-web/package.jsonand outputssurfc-web/dist/.- Astro performs static site generation with zero client JS by default. Fonts are bundled from
@fontsource/*npm packages withfont-display: optional(no Google Fonts CDN) to support long-lived cache headers (SUR-227). npm run devstartsastro dev;npm run previewserves the built site locally.
Hosting topology
- Netlify currently serves
surfc-web/dist/atsurfc.app. SPA routing is not needed (static pages;output: 'static'inastro.config.mjs; no@astrojs/cloudflareadapter). - Long-lived
Cache-Control: max-age=31536000, immutableheaders are applied to/_astro/*, favicons, and OG assets via bothnetlify.toml(SUR-227, Netlify-specific) andpublic/_headers(SUR-254, Cloudflare Pages-compatible). Both files intentionally coexist;netlify.tomlwill be removed after the Cloudflare Pages cutover is confirmed stable. .nvmrcpins Node 22 so Cloudflare Pages uses the same Node version as the current Netlify build (SUR-254).- The
www→apex 301 fromnetlify.tomldoes not move into_headersor_redirects(Cloudflare’s_redirectsis path-only); it will become a Cloudflare zone-level Redirect Rule at cutover time. - Runtime dependencies: the
waitlist-signupEdge Function (fromsurfc/), and the blog RSS feed at/rss.xml(static, no runtime dependency). - Blog posts at
src/content/blog/are baked into the static build atdist/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;recommendedpreset 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 forsurfc-web/(SUR-218/SUR-227). - Self-hosted fonts with long-lived cache headers on
surfc-web/(SUR-227). public/_headers+.nvmrcadded tosurfc-web/for a Cloudflare Pages cutover (SUR-254, 2026-04-27). Netlify remains the live host until cutover is confirmed.public/.well-known/assetlinks.jsondeclares URL handling + credential sharing for the Android TWA (com.surfc.app), with two registered fingerprints (release + debug/test keystores).
- Static build/deploy workflow via Vite + Netlify + Supabase environment variables (
- 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 removesnetlify.toml; thewww→apex redirect will be handled as a Cloudflare zone-level rule.
- Unknown:
- Enforcement of running migrations plus
npm run check:schemaand 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 localastro previewis not defined in committed config. - Exact timeline for Netlify → Cloudflare Pages cutover for
surfc-web/is not committed.
- Enforcement of running migrations plus