ARCHITECTURE — Frequently Asking Questions

Status: LIVING — must match _layouts/ _includes/ assets/js/ assets/css/ _sass/ supabase/ scripts/ .github/workflows/. Audited by check. Reconciled: 2026-10-02. How the site is built, how the systems fit together, and where to find each one. Rules live in SPEC §V, not here. Every name below was grepped; unbuilt parts say (planned, T.x).

1. What it is

A Jekyll blog on GitHub Pages. Static pages carry small vanilla-JS modules that talk straight to Supabase (auth + RLS’d tables) from the browser. Anything needing a secret runs in a Supabase Edge Function: AI question moderation (OpenAI), subscriber email (SMTP), and HMAC-authenticated endpoints called by GitHub Actions. Reader loop: read post → sign in by magic link → set display name on /welcome/ → react / comment / subscribe → ask a question on /ask/.

2. Layers

_posts/ _drafts/           content (Markdown + front matter)
_layouts/ _includes/       Liquid templates; default.html boots the Supabase client
*.md, admin/, auth/        pages (ask, welcome, signin, drafts, admin/moderation, auth/confirm)
assets/js/                 one IIFE module per feature (ES5, global var FAQ*); shared helpers faq-util.js
assets/css/main.scss       → built /assets/css/main.css; imports _sass/_*.scss partials (plain CSS)
supabase/functions/        Edge Functions (Deno TS), service-role access
supabase/supabase-schema.sql + migrations/   database, RLS, RPCs
.github/workflows/         build+deploy, notify, scheduled-post check, tests
scripts/                   dev setup, secrets, generators

Direction: page → assets/js module → Supabase (RLS) or Edge Function → DB. Browser code never holds a secret; only the publishable key.

Known violations (tolerated, planned, don’t copy):

  • Quota rule stated twice (edge submit-question/logic.ts constants, SQL get_question_quota / can_submit_question_today), kept equal by tests/quota-rule.test.js (SPEC D.6).
  • welcome.md inline script mirrors auth/profile/subscription logic now in faq-util.js / comments.js → T.17.
  • Files >400 lines: moba-sim-labs.js 614, chart-fullscreen.js 517, drafts.html 409, accepted for now: split on the next substantive change (T.14), capped shrink-only by tests/split-modules.test.js. chart-fullscreen.js has no tests → T.22.
  • PROD lags the repo until migration 018 is applied: profiles.premium self-writable (GH #209), record_send_failure callable by anon (GH #210) → T.13 open item.

3. Systems

3.1 Site build and deploy

Concern File / symbol
Config (prod) _config.yml (Supabase url/key empty; filled at build)
Config (dev) _config.dev.yml (dev URL + publishable key, committed)
Build + deploy .github/workflows/jekyll.yml job build (jekyll build --drafts, writes _config_production.yml from secrets)
Local dev scripts/serve-dev.sh → localhost:4000
Supabase client boot _layouts/default.html (loads SDK from site.supabase.sdk_url with SRI)

3.2 Auth and profiles

Concern File / symbol
Sign-in page (magic link) signin.md
Token landing auth/confirm.html
Profile setup, subscription toggle, badges welcome.md (inline script, onAuthStateChange)
Profile creation RPC create_profile(username)
Admin check RPC is_admin() over admin_users

3.3 Engagement (reactions, comments, subscribe)

Concern File / symbol
Module assets/js/comments.js (FAQComments: engagement bar, reactions, subscribe toggle, comment form, boot)
Thread assets/js/comment-thread.js (FAQCommentThread: load, nest, render, reply target)
URL auth errors assets/js/auth-errors.js (FAQAuthErrors.checkUrl: banner + copyable debug info)
Shared helpers assets/js/faq-util.js (FAQUtil: esc, $, timeAgo, getUser, getProfile, bootOnAuthChange); also used by 3.4, 3.5
Markup _includes/comments.html, _includes/subscribe.html (included by _layouts/post.html)
Reaction counts RPC get_reaction_counts(slug)
Tables reactions, comments (status visible/flagged/hidden), subscriptions

3.4 Ask a question

Concern File / symbol
Page ask.md → assets/js/ask.js (FAQAsk: quota, submit form, boot) + ask-questions.js (FAQAskQuestions: list, votes)
Quota UI ask.js getQuotaInfo → RPC get_question_quota; on error “could not be checked”, form stays (server enforces, V.3)
Submit ask.js functions.invoke("submit-question")
Server supabase/functions/submit-question/index.ts (wiring) → handler.ts (flow) → logic.ts (pure rules)
Duplicate detection data submit-question/article-summaries.json ← scripts/generate-article-summaries.js
Votes table question_votes, RPC get_question_vote_counts(q_id)

Talks to: OpenAI chat completions (two calls: article-overlap check, moderation).

3.5 Admin moderation

Concern File / symbol
Page admin/moderation.md → assets/js/admin-moderation.js (FAQAdminModeration: gate, data, tabs, save) + admin-moderation-view.js (FAQAdminModerationView: stateless cards, filter bar, paging)
Writes direct questions / comments updates, allowed by RLS via is_admin()
Server-side alternative supabase/functions/moderate-question/index.ts (HMAC; no in-repo caller, kept for server-to-server use, SPEC I.4)

3.6 Subscriber notifications

Concern File / symbol
On push to main .github/workflows/jekyll.yml job notify (new _posts/*.md in last 5 commits)
Daily for scheduled posts .github/workflows/check-scheduled-posts.yml (cron 30 4 * * *)
Already-sent check supabase/functions/check-notification-status/index.ts
Send supabase/functions/notify-subscribers/index.ts (SMTP, retries, 207 partial)
Dedup / audit notification_log (per post_slug, the dedup key); notification_recipients (audit only, not read)
Failure tracking RPC record_send_failure (auto-blacklist at 3)

3.7 Raw guides

scripts/build-raw-guide.js turns a post + guides/<name>/prep-repo/ into guides/<name>/GUIDE.md and prep-repo.tar.gz. Served at fixed URLs (SPEC §C). Guard tests/raw-guide.test.js.

4. Patterns in this codebase

Pattern What it means here Example
IIFE module + global var FAQX = (function () { … return {init: …}; })();, ES5 only; helpers it needs load first via <script> (V.19) assets/js/ask.js:4, ask.md
Supabase-gated markup Liquid renders auth scripts only when site.supabase.url and key are set _layouts/default.html:36
Self-authenticating Edge Function verify_jwt = false + own check (auth.getUser(jwt) or HMAC) supabase/config.toml, submit-question/handler.ts (getBearerToken → auth.getUser)
Pure core + injected I/O rules in a pure logic.ts; request flow in handler.ts takes Supabase/OpenAI/clock as deps; index.ts only wires env + Deno.serve; Deno tests fake the deps supabase/functions/submit-question/
HMAC machine auth X-Notify-Token: <ts>.<hex>; signed with openssl dgst -hmac in workflows jekyll.yml:163 ↔ supabase/functions/_shared/notify-token.ts verifyNotifyToken (imported by the 3 HMAC functions)
Generated + committed generator script, CI fails on drift article-summaries.json, guides/*/GUIDE.md
Source-text tests many tests regex the source (schema, functions) rather than run it; weak guards tests/subscription.test.js
DOM tests jsdom loads a module (after its helpers) with a stubbed supabase tests/comments.test.js, tests/ask.test.js

5. Cross-cutting concerns

Persistence

| Store | Table / key | Contents | Migration notes | |—|—|—|—| | Supabase | see SPEC §C | all user data | edit via new supabase/migrations/0NN_*.sql; never rename an applied file; keep supabase-schema.sql = full state | | Browser | Supabase SDK session in localStorage | auth session | managed by SDK |

Randomness and time

| Where | Seeded / injected? | Guard | |—|—|—| | assets/js/moba-sim-labs.js (post simulation) | params.seed optional, else Math.random | tests/moba-sim-labs.test.js | | submit-question quota window | deps.now injected into createSubmitQuestionHandler | submit-question/handler_test.ts “V.3 …” | | HMAC window (_shared/notify-token.ts) | nowSeconds parameter, defaults to Date.now() | supabase/functions/_shared/notify-token_test.ts (T.5) | | Future-post skip | Date.now() inline, not injectable | unguarded |

6. “Where do I find X?”

I want to… Go to
Change AI moderation thresholds / prompt thresholds / routing: supabase/functions/submit-question/logic.ts (routeStatus, decideOutcome, applyDuplicateCap); prompt: index.ts runAIModeration
Change daily question quota submit-question/logic.ts (PREMIUM_DAILY_QUESTIONS, REGULAR_DAILY_QUESTIONS, QUOTA_WINDOW_HOURS) and a new migration redefining can_submit_question_today + get_question_quota (+ supabase-schema.sql); tests/quota-rule.test.js fails until both agree
Add an Edge Function supabase/functions/<name>/index.ts + [functions.<name>] in supabase/config.toml
Change the schema new supabase/migrations/0NN_*.sql + update supabase/supabase-schema.sql; behavioural SQL test in supabase/tests/ on a local DB (docs/live/dev-environment.md); PROD apply: docs/live/operations.md “Applying a migration”
Change the email unsubscribe flow unsubscribe.md + assets/js/unsubscribe.js (FAQUnsubscribe), legacy redirect in _layouts/default.html, link in notify-subscribers/index.ts (V.22)
Change the post page _layouts/post.html, _includes/*
Change styles the _sass/_*.scss partial for that area (plain CSS; new partial → @import in assets/css/main.scss); rules: .github/copilot-instructions.md
Write a post _posts/YYYY-MM-DD-slug.md, then npm run build:article-summaries (docs/live/content-authoring.md)
Run / fix PROD (deploy, secrets, admins, premium, notifications) docs/live/operations.md
Test users / dev DB scripts/setup-test-users.sh, scripts/sync-prod-to-dev.sh (broken, GH #219)
Understand why a decision was made docs/journal/
Know which rule must hold SPEC.md §V

7. End to end: one question submission

  1. /ask/ loads; FAQAsk gets session + profile, calls get_question_quota — assets/js/ask.js.
  2. Submit → functions.invoke("submit-question", {question_text, is_retry}) — ask.js:169.
  3. Function validates bearer, length, user, profile; counts rows in last 24 h — handler.ts createSubmitQuestionHandler (getBearerToken, validateQuestionText, dailyQuestionLimit, quotaWindowStartIso in logic.ts).
  4. Fuzzy-match against article-summaries.json, AI overlap check on candidates ≥20 — logic.ts getFuzzyArticleMatches, selectArticleCandidates; index.ts runAIArticleCheck.
  5. AI moderation → score; duplicate cap at 70 — index.ts runAIModeration, logic.ts parseAIEvaluation, applyDuplicateCap.
  6. Route: first-attempt 20–80 returns rewrite unsaved; else insert into questions — logic.ts decideOutcome, insert in handler.ts.
  7. ask.js renders status, rewrite, related articles; flagged rows wait in /admin/moderation/.

8. Tooling

  • Tests: npm test (node:test + jsdom); CI .github/workflows/test.yml builds with dev config first, then runs both suites.
  • Edge Function unit tests: npm run test:deno (supabase/functions/**/*_test.ts, Deno 2, node:assert, no network); CI step after npm test.
  • Generators: npm run build:article-summaries, npm run build:raw-guides.
  • Review: scripts/review/codex_review.sh <range> (read-only Codex).
  • Secrets: scripts/vault/{store,fetch}-env-dev.sh (human-run, Bitwarden).
  • Real-app check: npm run e2e (scripts/e2e/smoke.js, Playwright; docs/live/e2e_testing.md). Signed-in scenarios need DEV running (T.9 🔶).