ARCHITECTURE — Frequently Asking Questions
ARCHITECTURE — Frequently Asking Questions
Status: LIVING — must match
_layouts/ _includes/ assets/js/ assets/css/ _sass/ supabase/ scripts/ .github/workflows/. Audited bycheck. 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.tsconstants, SQLget_question_quota/can_submit_question_today), kept equal bytests/quota-rule.test.js(SPEC D.6). welcome.mdinline script mirrors auth/profile/subscription logic now infaq-util.js/comments.js→ T.17.- Files >400 lines:
moba-sim-labs.js614,chart-fullscreen.js517,drafts.html409, accepted for now: split on the next substantive change (T.14), capped shrink-only bytests/split-modules.test.js.chart-fullscreen.jshas no tests → T.22. - PROD lags the repo until migration 018 is applied:
profiles.premiumself-writable (GH #209),record_send_failurecallable 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
/ask/loads;FAQAskgets session + profile, callsget_question_quota—assets/js/ask.js.- Submit →
functions.invoke("submit-question", {question_text, is_retry})—ask.js:169. - Function validates bearer, length, user, profile; counts rows in last 24 h —
handler.tscreateSubmitQuestionHandler(getBearerToken,validateQuestionText,dailyQuestionLimit,quotaWindowStartIsoinlogic.ts). - Fuzzy-match against
article-summaries.json, AI overlap check on candidates ≥20 —logic.tsgetFuzzyArticleMatches,selectArticleCandidates;index.tsrunAIArticleCheck. - AI moderation → score; duplicate cap at 70 —
index.tsrunAIModeration,logic.tsparseAIEvaluation,applyDuplicateCap. - Route: first-attempt 20–80 returns rewrite unsaved; else insert into
questions—logic.tsdecideOutcome, insert inhandler.ts. ask.jsrenders status, rewrite, related articles; flagged rows wait in/admin/moderation/.
8. Tooling
- Tests:
npm test(node:test + jsdom); CI.github/workflows/test.ymlbuilds 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 afternpm 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 🔶).