CLAUDE.md — AI Agent Guide
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.
| 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:
- Always add a
[functions.<name>]block toconfig.tomlwithverify_jwt = false - Add a comment explaining the auth strategy
- Deploy with
--no-verify-jwtflag
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-nocheckacceptable. - 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
- Create
supabase/functions/<name>/index.ts - Add to
supabase/config.toml:[functions.<name>] # <describe auth strategy here> verify_jwt = false - Deploy:
supabase functions deploy <name> --project-ref lbefrzvyznaoaglldluk --no-verify-jwt - Set secrets:
supabase secrets set --project-ref lbefrzvyznaoaglldluk KEY=value