Questions & Answers System
Full-stack Q&A pipeline: search → classify → upsert → answer (AI/community/admin) → moderate → publish.
Architecture Overview
User search query
→ questionClassifier.ts (client-side)
→ upsert-question (edge fn: normalize, hash, classify, deduplicate)
→ QuestionsSearchSection renders:
├── Answers grouped by type (bullet → editorial → community → AI)
├── Info-palaset (info blocks attached to the question — see below)
├── CommunityAnswerForm (auth-gated)
├── QuestionConsentBlock (AI generation / human request)
├── AnchorQuestionsSection (topic-linked curated Q&A)
└── SimilarQuestionsSection (trigram similarity)
Key Files
Edge Functions
| Function | Path | Purpose |
|----------|------|---------|
| upsert-question | supabase/functions/upsert-question/ | Normalize, classify, upsert, find similar |
| generate-ai-answer | supabase/functions/generate-ai-answer/ | AI answer via Claude (paid, quota-checked) |
| submit-community-answer | supabase/functions/submit-community-answer/ | User-submitted answer (free, needs admin approval); bot-gated |
| submit-feedback | supabase/functions/submit-feedback/ | Anonymous app feedback (bot-gated; replaces the old direct anon insert) |
| vote | supabase/functions/vote/ | +1/-1 voting on questions/answers |
All three anon-callable submit functions (upsert-question, submit-community-answer,
submit-feedback) run _shared/verify-human.ts (honeypot + Turnstile + throttle) first.
Hooks (apps/raamattu-nyt/src/hooks/)
| Hook | File | Purpose |
|------|------|---------|
| useUpsertQuestion | useQuestions.ts | Mutation: upsert question via edge fn |
| useGenerateAiAnswer | useQuestions.ts | Mutation: generate AI answer (quota-checked) |
| useSubmitCommunityAnswer | useQuestions.ts | Mutation: submit community answer |
| useVote | useQuestions.ts | Mutation: vote on question/answer |
| usePublishedQuestion | useQuestions.ts | Query: single published question by slug |
| useQuestionAnswers | useQuestions.ts | Query: published answers for question |
| useAdminQuestions | useQuestions.ts | Query: all questions with answer stats |
| useAdminAnswers | useQuestions.ts | Query: all answers (including unpublished) |
| useAnchorQuestions | useAnchorQuestions.ts | Query: anchor Q&A grouped by topic |
Components (apps/raamattu-nyt/src/components/search/)
| Component | Purpose |
|-----------|---------|
| QuestionsSearchSection | Main Q&A display in search results |
| QuestionConsentBlock | AI answer / human request consent |
| CommunityAnswerForm | User answer submission form |
| SimilarQuestionsSection | Trigram-similar questions |
| AnchorQuestionsSection | Topic-linked curated Q&A |
Pages
| Page | Route | Purpose |
|------|-------|---------|
| QuestionPage.tsx | /kysymys/:slug | Public single question view + admin tools |
| AdminQuestionsPage.tsx | /ohjaamo/questions | Full admin Q&A dashboard |
| PremiumQuestionsPage.tsx | /premium/questions | Public questions browsing |
| AdminGuestAuthorsPage.tsx | /ohjaamo/guest-authors | CRUD for Vieraskynä (guest) authors |
Core Libraries
| File | Purpose |
|------|---------|
| src/lib/questionClassifier.ts | Client-side question detection (Finnish) |
Database Schema (bible_schema)
For full schema details: See references/schema.md
Tables
- questions — Normalized questions with classification, status, voting
- answers — Answers with author_type, rich content, key_verses, topics, strongs_refs
- votes — User votes (+1/-1) on questions/answers (unique per user)
- topic_anchor_questions — Links questions as anchor Q&A for topics
Answer Author Types
| Type | Created by | Published by default? |
|------|-----------|----------------------|
| bullet | Admin | Yes |
| admin | Admin | Yes |
| system | Import/seed | Yes |
| ai | Edge function | No (needs admin review) |
| community | Authenticated user | No (needs admin approval) |
| guest | Admin (Vieraskynä) | Yes — attributed to a guest_authors row via guest_author_id |
Author display: account authors (author_id) resolve their public name via the
get_author_public_info RPC (profiles RLS is admin-or-self); guest authors (guest_author_id)
show display_name/@username from guest_authors. See Vieraskynä.
Question Status Flow
draft → needs_review → approved → published
└→ rejected
RLS Summary
- Anon/Auth: Read published questions + published non-hidden answers
- Auth: Read own questions + own community answers, insert votes, submit community answers
- Admin: Full CRUD on all questions, answers, votes
Common Patterns
Add new answer type
- Add CHECK constraint value in migration
- Update
AnswerDatainterface inQuestionsSearchSection.tsx - Update
Answertype inuseQuestions.ts - Add rendering logic in
renderAnswerCard - Update admin management in
AdminQuestionsPage.tsx
Add new question classification
- Update classifier patterns in
questionClassifier.ts - Mirror logic in
upsert-questionedge function - Update CHECK constraint if new DB value needed
- Add UI handling in
QuestionsSearchSection
Modify AI answer generation
- Edit prompt template via
ai-prompt-managerskill - Edge function:
generate-ai-answer/index.ts - Output validation: excerpt, key_verses, topics, strongs_refs, related_questions
- Model/vendor configured via
ai_feature_bindingstable
Add feature to community answers
- Edge function:
submit-community-answer/index.ts - Hook:
useSubmitCommunityAnswerinuseQuestions.ts - Form:
CommunityAnswerForm.tsx(search) or inline inQuestionPage.tsx - Admin tab: "Yhteisövastaukset" in
AdminQuestionsPage.tsx
Vieraskynä (guest authors)
Account-less named attribution for answers AND info-palat. An admin can credit a
piece to a bible_schema.guest_authors row (a named contributor with no login) via
guest_author_id + author_type='guest', instead of a real account.
| Concern | File |
|---------|------|
| List/CRUD hook | hooks/useGuestAuthors.ts (RPC get_guest_authors for active list; bibleSchemaTable("guest_authors") for admin CRUD) |
| Admin picker | components/admin/GuestAuthorSelect.tsx (used in NewAdminAnswerForm, InfoBlockEditor) |
| Admin page | pages/AdminGuestAuthorsPage.tsx (/ohjaamo/guest-authors) |
| Public name resolve | hooks/useUsernamesByIds.ts → RPC get_author_public_info(uuid[]) |
| Render | question-detail/AnswerCard.tsx, question-detail/InfoTextCard.tsx |
Gotcha: account authors' public names come from get_author_public_info (SECURITY
DEFINER) because public.profiles SELECT is admin-or-self — a direct profiles read returns
nothing to guests, so named authors would silently not render. The old account-only
AuthorSelect.tsx + lib/designatedAuthors.ts were removed — don't reference them.
Anonymous-submission bot protection
Anon submit paths (upsert-question, submit-community-answer, submit-feedback) gate
on supabase/functions/_shared/verify-human.ts: honeypot (empty hidden field + ≥1500 ms
on screen), Cloudflare Turnstile (verified via TURNSTILE_SECRET_KEY; skipped when the
secret is unset, so dev/staging just work), and a rate-limit (check_and_record_throttle
→ public.submission_throttle, service_role-only). Client widget/fields:
components/security/HumanCheckFields.tsx + useHumanCheck.ts (wired into
QuestionConsentBlock, AskQuestionButton, AppFeedbackForm). When adding a new anon
submit path, add the fields client-side and call verifyHuman(...) server-side.
AI answers hidden from users
AI answer generation still exists (ai answer type, generate-ai-answer) but its UI is
hidden from non-admin users — lib/aiVisibility.ts#shouldHideAIForUser(plan) returns true
for everyone except admin (AI quotas are zeroed for all user plans). Gate any AI
button/panel/token display behind it. Exception: plan-from-summary (kooste→lukusuunnitelma)
is NOT gated — it stays visible for Pro/Premium.
Info-palaset (info blocks)
Reusable content blocks (bible_schema.info_blocks) attached to questions, topics, and
spiritual paths. One of two answer mechanisms for a question (alongside community
answers). Admin-curated — no AI generation and no classifier (unlike questions).
[!important] Key invariant An info-pala is rendered by TWO components:
InfoTextCard(page-tila) andInfoView(cinema-tila). Edit whichever the request targets and keep both in sync.
Tables (bible_schema)
- info_blocks —
slug,title_fi/en,body_md/en,body_html/en,excerpt/en,is_published,is_handled,is_hidden,is_core(gates attachability),author_type(admin/community/system),icon,bg_color,image_url,key_verses(display-only),key_prayers,action_links(route/url/grand_plan; legacyaction_grand_plan_id). - question_info_blocks / topic_info_blocks / spiritual_path_info_blocks — junctions
(
sort_order; paths also carrybinding_id).
RPCs
get_info_blocks_by_slugs(p_slugs)— public, published blocks by slugget_attachable_info_blocks()— admin-only (SECURITY DEFINER), core blocks + attach countsget_spiritual_path_info_block_slugs[_admin]()— path bindings
Key files
| Concern | File |
|---------|------|
| CRUD hook | hooks/useInfoBlocks.ts |
| Fetch by slug | hooks/info-blocks/useInfoBlocksBySlugs.ts |
| Question-attached | hooks/questions/useQuestionInfoBlocks.ts (+ admin variant) |
| Topic-attached | hooks/topics/useTopicInfoBlocks.ts |
| Path bindings | hooks/info-blocks/useSpiritualPathInfoBlock{Bindings,Mutations}.ts |
| Page render (1 block) | pages/question-detail/InfoTextCard.tsx (wrapper QuestionInfoSection.tsx) |
| Cinema render | components/cinema/InfoView.tsx (1) · InfoFlow.tsx (snap-scroll N) · InfoCinema.tsx (shell) |
| Attach dialog | pages/question-detail/AddInfoBlockDialog.tsx · components/info-blocks/InfoBlockAttachDialog.tsx |
| Action links | components/info-blocks/InfoBlockActions.tsx |
| Path render | components/spiritual-path/SpiritualPathInfoBlock.tsx |
| Admin | pages/AdminInfoBlocksPage.tsx (/ohjaamo/info-blocks) · pages/admin-info-blocks/InfoBlockEditor.tsx |
Q&A cinema integration
QuestionCinemaApp renders published community answers (via answerToInfoBlock()) +
attached info blocks through the same InfoFlow/InfoView — answers first, then info blocks.
Common patterns
- Create/edit an info-pala →
InfoBlockEditor(fill both FI + EN;is_coregates attachability). - Attach to question/topic/path → attach dialog → junction row with
sort_order. - Change appearance → page =
InfoTextCard, cinema =InfoView— edit both. - Info blocks are read via RPC + typed
bibleSchemaRpc(not direct.from("info_blocks")) → mock those in tests.
Full info-block schema + RLS: See references/schema.md.
Shared UI Components
- BibleReferencePickerPopover — Verse picker popover (used across 6 files)
- SafeTextarea — Input with XSS protection, markup detection
- RichContentRenderer — Renders body_html from rich editor
- VerseContentCard — Displays verse reference with content
- FeedbackThumbs / FeedbackText — Answer feedback (AI answers)
References
- Full schema + RLS policies: See references/schema.md
- Learnings / gotchas: See references/learnings.md