CLAUDE.md — AI Agent Guide

This file is the primary reference for AI agents working on this repo. Read it before making changes.


Project at a Glance

Frequently Asking Questions — Jekyll static blog backed by Supabase (Postgres + Edge Functions).
Live: https://frequently-asking-questions.com | Dev: https://lbefrzvyznaoaglldluk.supabase.co

Stack: Jekyll + vanilla JS · Supabase auth (magic link + password) · Supabase Edge Functions (Deno/TypeScript) · OpenAI GPT-4o-mini for question pre-moderation · GitHub Pages


MCP Access

Two Supabase MCP servers are configured in .mcp.json:

MCP alias Project Access
mcp__supabase__* DEV (lbefrzvyznaoaglldluk) Read + write
mcp__supabase_prod__* PROD (pxqcmkzvyapuhkqqlxgx) Read-only

Use mcp__supabase__execute_sql freely on dev. Use mcp__supabase__apply_migration for schema changes.
Never write to prod. Use the prod MCP only to read reference data.


Admin: Spinning Up the Dev Environment

One-time setup for a human:

# 1. Install Ruby deps
bundle install

# 2. Install Node deps
npm install

# 3. Ensure .env.dev is filled in
#    Already contains: OPENAI_API_KEY, NOTIFY_SECRET, SMTP_*, DEV_SERVICE_ROLE_KEY
#    Must fill in manually: PROD_SERVICE_ROLE_KEY (Supabase dashboard → Settings → API)

# 4. Push Edge Function secrets to dev Supabase
bash scripts/set-dev-secrets.sh

# 5. (First time only) Create test users
bash scripts/setup-test-users.sh

# 6. Run the site
bash scripts/serve-dev.sh
# → http://localhost:4000 with drafts, live reload, pointing at dev Supabase

.env.dev is gitignored (never commit it). It lives at the repo root.


Test Users (DEV only)

Four permanent test users exist in the DEV Supabase project. Run bash scripts/setup-test-users.sh to recreate them if needed.

Email Password Username Premium Admin Profile
test-agent@faq.local TestAgent123! TestAgent No No Yes
test-premium@faq.local TestPremium123! TestPremium Yes No Yes
test-admin@faq.local TestAdmin123! TestAdmin No Yes Yes
test-noprofile@faq.local TestNoProfile123! (none) No No

User IDs (stable):

test-agent@faq.local       1e32cf70-a920-4d4c-ab15-8a28c212e5a4
test-premium@faq.local     d95d7b4f-d699-4cc2-879b-dc263ffc29e5
test-admin@faq.local       fe32d41b-0ef6-46fd-a9b8-b538c410f93d
test-noprofile@faq.local   4f951afc-805b-4a45-a609-002b1ba3bf0a

What each user is for:

  • test-agent — Workhorse for most automated tests. Use for question submission, voting, comments, reactions, subscriptions.
  • test-premium — Tests premium-only features: /drafts/ access, different rate-limit messaging on /ask/.
  • test-admin — Tests the admin moderation dashboard at /admin/moderation/. Can view flagged/rejected questions and all comments.
  • test-noprofile — Tests the “set up your display name” gate. Signing in and visiting /ask/ should show the profile setup prompt instead of the question form.

Getting a JWT for Automated Tests

All test users support password-based sign-in (useful for agents and scripts — no magic link needed):

ANON="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxiZWZyenZ5em5hb2FnbGxkbHVrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUwNDMwMTksImV4cCI6MjA5MDYxOTAxOX0.h2aYxa_YH-KULYbEIVrK2Q7xKOFXapY15305OEBZmks"
BASE="https://lbefrzvyznaoaglldluk.supabase.co"

# Sign in as any test user:
SESSION=$(curl -s -X POST "$BASE/auth/v1/token?grant_type=password" \
  -H "apikey: $ANON" \
  -H "Content-Type: application/json" \
  -d '{"email":"test-agent@faq.local","password":"TestAgent123!"}')

ACCESS_TOKEN=$(echo "$SESSION" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

Replace the email/password to get tokens for other test users.


Testing in the Browser (Manual)

The site’s frontend uses magic link (OTP) auth. Test users in dev have password-based auth enabled, but the signin page only offers magic links. To sign in as a test user in the browser, generate a magic link via the Supabase dashboard or CLI and open it:

# Generate a one-click login link for a test user (open in browser immediately — single-use)
SERVICE_ROLE="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxiZWZyenZ5em5hb2FnbGxkbHVrIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NTA0MzAxOSwiZXhwIjoyMDkwNjE5MDE5fQ.GTexhXYawX0_tis3yq8hkrQb_RbTDzMuS8rwR646c-g"
BASE="https://lbefrzvyznaoaglldluk.supabase.co"

curl -s -X POST "$BASE/auth/v1/admin/generate_link" \
  -H "apikey: $SERVICE_ROLE" \
  -H "Authorization: Bearer $SERVICE_ROLE" \
  -H "Content-Type: application/json" \
  -d '{"type":"magiclink","email":"test-admin@faq.local","redirect_to":"http://localhost:4000/welcome/"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['action_link'])"

Open the printed URL in the browser — it logs you in immediately. The link is single-use; generate a fresh one each time.

For automated/agent testing, skip all of this and use the password API directly (see “Getting a JWT” above).


Test Scenarios by Page

/ask/ — Ask a Question

User Expected behaviour
Not signed in “Sign in to submit and vote” prompt
test-noprofile “Set up your display name” prompt
test-agent Question submission form
test-agent (already submitted today) Rate-limit message (no upgrade prompt)
test-premium (already submitted today) Rate-limit message without the upgrade prompt
Any signed-in user Questions list visible; voting works

/welcome/ — Profile Setup

User Expected behaviour
Not signed in Redirected to /signin/
test-noprofile Username setup form
test-agent “Welcome, TestAgent!” + subscription toggle
test-premium “Welcome, TestPremium!” + ✨ Premium Member badge + draft link
test-admin “Welcome, TestAdmin!” + 🔒 Administrator badge + link to /admin/moderation/

/admin/moderation/ — Admin Dashboard

User Expected behaviour
Not signed in “This dashboard is only available to admins”
test-agent “This dashboard is only available to admins”
test-admin Full dashboard with Questions and Comments tabs

Edge function: submit-question

# Should succeed (200) with a question score + status:
curl -s -X POST "$BASE/functions/v1/submit-question" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"question_text":"How many calories does the average person burn just by existing?"}'

# Should return 429 on second call within 24h
# Should return 401 with no Authorization header
# Should return 400 with question_text shorter than 10 chars

Resetting Test Data

Between test runs, clear state with SQL via MCP or the browser button on /dev-login/:

-- Delete all questions for a test user (resets the 24h rate limit)
DELETE FROM public.questions
WHERE user_id = '1e32cf70-a920-4d4c-ab15-8a28c212e5a4'; -- test-agent

-- Or clear all test users at once:
DELETE FROM public.questions
WHERE user_id IN (
  '1e32cf70-a920-4d4c-ab15-8a28c212e5a4',
  'd95d7b4f-d699-4cc2-879b-dc263ffc29e5',
  'fe32d41b-0ef6-46fd-a9b8-b538c410f93d',
  '4f951afc-805b-4a45-a609-002b1ba3bf0a'
);

-- Delete test votes:
DELETE FROM public.question_votes
WHERE user_id IN (
  '1e32cf70-a920-4d4c-ab15-8a28c212e5a4',
  'd95d7b4f-d699-4cc2-879b-dc263ffc29e5',
  'fe32d41b-0ef6-46fd-a9b8-b538c410f93d'
);

Critical: ES256 JWT + verify_jwt (Fixed)

New Supabase projects issue ES256 (asymmetric) JWTs. Edge functions deployed with verify_jwt = true (the default) run a legacy HS256 gateway check that rejects all ES256 tokens with 401 Invalid JWT before the function code ever runs.

This was the root cause of the ask-a-question feature being completely broken.

Fix: supabase/config.toml now sets verify_jwt = false for submit-question (and all other user-facing functions). The function validates auth itself via supabase.auth.getUser(jwt) — security is unchanged.

Rule for all future edge functions:

  1. Always add a [functions.<name>] block to config.toml with verify_jwt = false
  2. Add a comment explaining the auth strategy
  3. Deploy with --no-verify-jwt flag

Do not trust that verify_jwt = true will work. It will silently break for ES256 users.


Edge Function Deployment

# Deploy single function (always use --no-verify-jwt for user-facing functions)
supabase functions deploy submit-question \
  --project-ref lbefrzvyznaoaglldluk \
  --no-verify-jwt

# Deploy all functions
supabase functions deploy \
  submit-question moderate-question notify-subscribers check-notification-status \
  --project-ref lbefrzvyznaoaglldluk

# View live logs
supabase functions logs submit-question --project-ref lbefrzvyznaoaglldluk

# List current secrets
supabase secrets list --project-ref lbefrzvyznaoaglldluk

Key Files

CLAUDE.md                                   ← you are here
supabase/
  config.toml                               edge function settings (verify_jwt, etc.)
  functions/
    submit-question/index.ts                AI-moderated question submission
    submit-question/article-summaries.json  regenerate: npm run build:article-summaries
    moderate-question/index.ts              admin moderation (HMAC-authenticated)
    notify-subscribers/index.ts             email on new posts
    check-notification-status/index.ts      dedup check for notifier
  migrations/
    010_add_admin_moderation_system.sql
    011_add_ai_question_moderation_fields.sql
    012_allow_retries_after_rejection.sql
assets/js/
  ask.js                                    /ask/ page — form, submission, voting
  comments.js                               engagement bar + comments
  admin-moderation.js                       admin dashboard
_config.yml                                 Supabase URL/key empty (filled at build time)
_config.dev.yml                             dev Supabase URL + anon key (committed, safe)
.env.dev                                    dev secrets (gitignored — never commit)
scripts/
  serve-dev.sh                              run site at localhost:4000
  setup-test-users.sh                       create/recreate all test users in dev
  set-dev-secrets.sh                        push .env.dev secrets to edge functions
  sync-prod-to-dev.sh                       overwrite dev DB with prod data copy
  generate-article-summaries.js             regenerates article-summaries.json

Database Schema

All tables have RLS enabled. The edge functions use SUPABASE_SERVICE_ROLE_KEY (auto-injected) to bypass RLS.

Table Purpose
profiles Display names; premium flag
questions Submitted questions + AI moderation fields
question_votes Up/downvotes; unique per (user, question)
comments Post comments; status: visible/flagged/hidden
reactions like/dislike per post per user
subscriptions Email subscriber list
admin_users Admin grant/revoke; checked by is_admin()
notification_log Dedup for subscriber email sends

Key RPCs (callable with supabase.rpc()):

Function Notes
can_submit_question_today() Bool; rate-limit check; SECURITY DEFINER
create_profile(username text) Idempotent; ON CONFLICT (id) DO NOTHING
get_question_vote_counts(q_id uuid) Returns upvote/downvote/net_votes
is_admin() Bool; SECURITY DEFINER; used by RLS policies

Article Summaries (Duplicate Detection)

supabase/functions/submit-question/article-summaries.json must stay in sync with _posts/:

npm run build:article-summaries
git add supabase/functions/submit-question/article-summaries.json
git commit -m "chore: update article summaries"

CI fails if the committed file is stale.


Running Tests

npm test    # Jest: build-output, comments, subscription unit tests

CI (.github/workflows/jekyll.yml) runs on every push: regenerates summaries, builds, deploys, notifies subscribers.


Code Style

  • Frontend JS: Vanilla ES5-compatible. IIFE modules. No bundler, no TypeScript.
  • Edge functions: TypeScript (Deno). // @ts-nocheck acceptable.
  • CSS: Plain CSS, BEM-like naming, all in assets/css/main.css.
  • HTML: Liquid templates in _layouts/ and _includes/.

Visual design rules (colour palette, typography, focus states): see .github/copilot-instructions.md.


Adding New Edge Functions

  1. Create supabase/functions/<name>/index.ts
  2. Add to supabase/config.toml:
    [functions.<name>]
    # <describe auth strategy here>
    verify_jwt = false
    
  3. Deploy: supabase functions deploy <name> --project-ref lbefrzvyznaoaglldluk --no-verify-jwt
  4. Set secrets: supabase secrets set --project-ref lbefrzvyznaoaglldluk KEY=value