Bible Quest origin — an annotated walkthrough
Status: 🟩 COMPLETE Last updated: 2026-06-21 Plain-English tagline: The actual story of how St Mark’s Bible Quest got built end-to-end, phase by phase, with every encyclopedia entry that explains a piece of it. Half case study, half stress-test of the encyclopedia.
What this is
A case study: the real story of how St Mark’s Bible Quest got built, with links to the encyclopedia entries that explain each piece. It works two ways:
- If you’re reading the encyclopedia and want to see how it all comes together in a real project — start here.
- If you’re starting your own project and want a reference for the order of operations — this is the playbook in narrative form.
The project: St Mark’s Bible Quest is a Bible-study webapp for kids at St Mark’s church. Daily passages, quizzes, streaks, badges, levels, leaderboards, memory verse practice, word search, treasure hunts, team challenges, themed days, weekly prizes — 17 features + bonus features, shipped in roughly 10 days of focused work, by a non-programmer (George) collaborating with Claude Code.
Live URL: https://stmarkbible.com
This walkthrough covers the build from kickoff through the Reading Plans feature (May 7 to May 16, 2026), plus the domain-registration milestone in May 17. Iteration since then has continued; for the authoritative current state, see PROGRESS.md inside the project.
Phase 0 — Picking the stack and tools (May 7)
The first session was almost entirely about choices. Not code — choices.
The stack we landed on:
- Next.js 16 🟩 🟦 for the framework
- Tailwind v4 🟩 🟦 + ui 🟩 🟦 for styling
- Supabase 🟩 🟦 for database + auth
- Vercel 🟩 🟦 for hosting
- GitHub 🟩 🟦 for source control
- TypeScript 🟩 throughout
This is the stack the encyclopedia now treats as default for any new webapp. It exists because of this project. The reason is brutally pragmatic: this stack is the fastest path from zero to shipping a real webapp for a solo developer (or a non-coder driving Claude Code).
Encyclopedia entries that explain this phase:
- What is a program? 🟩, What is a computer? 🟩, How the web works 🟩 — the foundational mental model
- Client vs server 🟩 — why Next.js’s server/client split matters from day one
- Reading path: Build your first webapp 🟩 — the curated trail through these same decisions
Lesson surfaced: picking the stack before any code is written takes one session and saves weeks. Defer decisions until the moment they bite, then they cost hours.
Phase 1 — Local environment + scaffolding (May 7)
Same day. Installed Node.js, Git, VS Code. Set PowerShell execution policy. Created the project with npx create-next-app@latest. Initialized git. Pushed to GitHub. Created the Vercel project. Live URL minted: https://st-marks-bible-quest.vercel.app/.
The whole flow — from “nothing installed” to “default Next.js page live on the internet” — was about 90 minutes.
Encyclopedia entries that explain this phase:
- Operating systems intro 🟩 and command line 🟩 — the OS and shell layer
- PowerShell vs Bash 🟩 — the Windows-specific quirks
- Files and folders 🟩 —
.env.local,.gitignore, project structure - npm & package managers 🟩 —
npx create-next-app, dependencies - Node.js runtime 🟩 — what’s actually running underneath
- Git basics 🟩 and GitHub 🟩 🟦 — the initial commit and push
- Windows dev environment 🟩 — the setup gotchas Windows users hit
- How-to: Start a new Next.js project (with the playbook) 🟩 — the procedural version
Lesson surfaced: the single gotcha that ate the most time was PATH refresh in background PowerShell sessions. Documented in project_tech_stack memory and in PowerShell vs Bash — read it before your first long-running command in a fresh PowerShell session.
Phase 2 — Database + auth (May 7–8)
Supabase project created. Schema designed: 6 core tables (users, passages, questions, submissions, reading_logs, points_log). Migration 20260507_001_initial_schema.sql written and run in the Supabase SQL Editor.
A critical decision early: custom code-based auth, NOT Supabase Auth. Kids each get an 8-character access code in the format XXXX-XXXX. Login = type the code. Server validates, issues a session cookie. A sessions table tracks active logins.
Why custom auth?
- Kids don’t have email addresses to receive magic links
- Server-side sessions are simpler than JWTs for this use case
Encyclopedia entries that explain this phase:
- What is a database? 🟩, Postgres 🟩, SQL the language 🟩
- Supabase 🟩 🟦 — the platform underneath
- Schema design 🟩 and Migrations 🟩 — designing tables and tracking changes
- Row-Level Security 🟩 — RLS enabled on every table; server-side queries use
service_roleto bypass - Authentication vs authorization 🟩, Sessions and cookies 🟩, JWT 🟩 — the auth concepts (and why JWT wasn’t used)
- Secrets management 🟩 —
service_rolekey handling - Environment variables 🟩 — where the keys live
- How-to: Set up a Supabase project 🟩 and How-to: Enable Row-Level Security 🟩
Gotcha surfaced in real time: raw-SQL Supabase migrations need GRANT statements or service_role hits “permission denied” (Postgres error 42501). Fix lives in 20260507_002_grant_role_privileges.sql. The pattern is now a documented gotcha — push invariants into the database, but remember the database still needs to grant access to the role that’s using it.
Phase 3 — MVP build (May 8–9)
About 36 hours of elapsed time. Shipped:
/admin/kids— create kids with auto-generated access codes (8 chars, formatXXXX-XXXX, retry on collision)/admin/passages— admin enters today’s daily Bible passage withreading_points/admin/passages/[id]/questions— write quiz questions per passage (MCQ / true-false / fill-in-blank)/home— kid sees today’s passage, clicks “Mark as Read” for points/quiz/[passageId]— interactive one-question-at-a-time quiz with per-answer feedback/leaderboard— weekly point standings
Encyclopedia entries that explain this phase:
- React 🟩, TypeScript 🟩 — the language layer
- Server Actions 🟩 —
markAsReadAction,submitAnswerAction— the verbs of the system - Next.js — Server vs Client Components 🟩 🟦 — every page reads from the DB on the server; no client-side fetch loop
- Tailwind CSS 🟩 🟦, ui 🟩 🟦 — the visual layer
- Forms and validation 🟩 — login form, admin forms, kid quiz inputs
- HTML 🟩, CSS 🟩, JavaScript 🟩, The DOM 🟩 — the foundations underneath JSX
Critical gotcha — timezones. Vercel runs in UTC. Admin is in Australia/Sydney. A passage dated 2026-05-09 (admin local) would be invisible to kids until midnight UTC (10am Sydney). Fix: lib/datetime.ts exports todayInProjectTimezone() which uses Intl.DateTimeFormat("en-CA", { timeZone }) to return YYYY-MM-DD in PROJECT_TIMEZONE. The whole project’s date logic now flows through this helper.
Critical gotcha — security pattern. The quiz correct_answer field is only sent to the client for questions the kid has already submitted. Otherwise kids could “View Page Source” to see future answers. The server enforces this at the data-fetch layer.
Encyclopedia entries on the gotchas:
- Regions & edge 🟩 — Vercel functions pinned to
syd1to match Supabase region for low latency - SPA vs MPA vs SSR vs SSG 🟩 — why “fetch on the server, render with the data” is the cleanest pattern
- How the web works 🟩 — “View Page Source” reveals client-side data; never put secrets in client code
- OWASP top 10 🟩 — broken access control is OWASP #1
- What is the internet? 🟩 — why Sydney↔︎US latency is real
Phase 4 — Phase 2 features: gamification (May 9)
In a single intense day, shipped 5 gamification features:
- Streaks with milestone bonuses (at 3, 7, 14, 30, 60, 100, 200, 365 days)
- Badges — 13 achievements seeded; idempotent awarder that runs on every relevant action
- Levels — 4 tiers (Disciple → Apostle → Scholar → Sage) derived from lifetime points
- Dark mode with cookie persistence and CSS-variable theming
- Custom avatars — 12 icons × 8 colors per kid
The badge system is worth highlighting: lib/badges.ts exports checkAndAwardBadges(supabase, userId) which:
- Pre-fetches all relevant data in parallel via
Promise.all - Computes derived metrics (reads count, perfect quizzes, etc.)
- Iterates a predicate map keyed by badge slug
- Awards any missing badges + their
points_valuebonus rows inpoints_log - Idempotent — safe to call from anywhere
Encyclopedia entries that explain this phase:
- JavaScript 🟩 — particularly array methods (
reduce,map,filter) used heavily in badge predicates - Async & concurrency 🟩 —
Promise.all([...])to parallelize the pre-fetches - Dark mode 🟩 — the cookie-based approach +
darkclass on<html>+ Tailwinddark:prefix - Dark mode design 🟩 — the design considerations behind the toggle
- How-to: Set up dark mode 🟩 — the procedural version
- UX principles 🟩 — gamification done right vs wrong
- Color theory for devs 🟩 — picking the badge rarity colors
Lesson surfaced: when adding a feature that retroactively rewards prior activity (badges, in this case), also call the awarder on page loads — not just from action handlers. Otherwise existing users have to perform fresh actions to backfill. This is now baked into the playbook.
Phase 5 — Phase 3 features: content + mini-games (May 9)
Same day, more features:
- Reflection journal per passage
- Audio reading — URL field; renders inline
<audio>for.mp3/.m4a/etc., falls back to “open in new tab” button for YouVersion links - Memory verse challenge — Read / Blanks / First-letters / Lock-in modes with Levenshtein-based similarity scoring
- Bible Character Match — daily card-flip game, seeded RNG so every kid gets the same daily challenge
- Phone wallpaper —
next/ogImageResponsegenerates a 1080×1920 PNG of the current memory verse - Treasure hunt — Bible-reference clues with a ~280-alias parser
- Word search — 12×12 grid built from a passage’s distinctive words
Encyclopedia entries that explain this phase:
- Algorithms intro 🟩 — Levenshtein distance for verse similarity scoring
- Data structures overview 🟩 — the 12×12 grid as a 2D array; word placement as constrained search
- Recursion 🟩 — used in
lineCellsline-validation - time and space complexity 🟩 — Levenshtein is O(m·n); fine for verse lengths, would be a problem at scale
- Memory, stack, heap 🟩 — image generation is a heap-allocation moment
- Forms and validation 🟩 — Lock-in textarea, treasure-hunt answer input
Lesson surfaced: YouVersion / bible.com URLs are HTML pages, not audio files — the <audio> element can’t play them. The right move is detecting URL type via regex on path extension and falling back to a “listen along (opens new tab)” link. Sometimes the right answer is a graceful fallback, not a fix.
Phase 6 — Phase 4 features: social (May 10)
5 social features in one day:
- Team challenges — kids assigned to teams; leaderboard with team standings
- High-five reactions — one per kid per recipient per day (
CHECK+UNIQUEenforced in the DB) - Discussion of the week — admin posts a question, kids respond, admin features the best
- Themed days — 7 daily themes (Mystery Monday, Trivia Tuesday, etc.)
- Real-world prizes — weekly prize announced + winner selected
The high_fives table has elegant constraints:
CHECK (from_user_id <> to_user_id)
UNIQUE (from_user_id, to_user_id, date_sent)These two lines enforce “you can’t high-five yourself” and “max one high-five per recipient per day” — without any application code involved.
Encyclopedia entries that explain this phase:
- SQL the language 🟩 — CHECK constraints, UNIQUE constraints,
ON DELETEbehavior - Joins and relationships 🟩 —
team_idforeign key, leaderboard aggregations - Transactions and ACID 🟩 — idempotent ledger writes
- Indexes and performance 🟩 — the indexes Supabase auto-creates for UNIQUE and FK columns
- SQL vs NoSQL 🟩 — why a relational store was the right call here
Lesson surfaced: push as much invariant enforcement into the database as possible. CHECK and UNIQUE are constant-time guarantees the application code can rely on. Adding the same logic in JavaScript would mean races, edge cases, and bugs.
Phase 7 — Reading Plans + ongoing iteration (May 14 onward)
The big post-MVP feature: Reading Plans. Admin creates a plan (“Read the Gospel of Matthew over 28 days”); the app generates the daily passages on its own, with optional AI quiz generation per passage. Frequency cadence (daily / weekdays-only / weekly+day), skip-dates, chain configs (auto-advance to the next book in a preset chain like “Whole NT”), end-of-book behaviors.
This phase brought in serious LLM work — the AI quiz generation pipeline:
- Cached system prompt for cost savings
- Per-passage Claude calls
- Idempotency via a row-level “already generated?” check
- 4-way concurrency batching for initial passage text fetches
Encyclopedia entries that explain this phase:
- What is an LLM? 🟩 — the basics
- The Claude API 🟩 🟦 — the API shape used for quiz generation
- Prompt engineering 🟩 — structuring the quiz-generation prompt
- Tool use 🟩 — Claude returns structured JSON for the quiz
- Tokens and context windows 🟩 — prompt caching for the static system prompt
- Claude models 🟩 🟦 — choosing Sonnet for the workhorse generation
- Agents 🟩 — the per-passage pipeline is a tiny agent
- Async & concurrency 🟩 —
Promise.allfor the 4-way concurrent fetches
Domain registration (May 17): bought stmarkbible.com through Vercel Domains. One-click setup; SSL auto-issued by Let’s Encrypt via Vercel. Live at https://www.stmarkbible.com.
Encyclopedia entries on the domain step:
Lessons that apply to any project
Distilling the lessons surfaced across these phases — these are now baked into the encyclopedia’s cross-project playbook:
- Pick the stack before any code. One session of decisions saves weeks of regret.
- Run
npm run buildbefore every push. Production is stricter than dev — TypeScript errors thatnpm run devskips will fail Vercel. - Pin the Vercel function region to match the database region. Cross-Pacific queries are slow for no reason.
- Use
service_roleonly server-side. Never expose it; never put it behindNEXT_PUBLIC_. - Enable RLS on every table containing user data. The default Supabase posture.
- Push invariants into the database when you can.
CHECKandUNIQUEare free; application-level checks are flaky. - Custom auth is fine for non-standard cases. Kids without emails, internal tools, niche use cases — server-side sessions are simpler than fighting a third-party auth system.
- Use UTC at the storage layer; convert at the boundary. All dates stored as UTC, all reads use a
todayInProjectTimezone()helper. - Make retroactive features run on page load too. Action-only awarders miss existing users.
- Detect URL types before assuming format. YouVersion looks like an audio link but isn’t.
- Idempotency everywhere it makes sense. Badge awards, point ledger entries, plan generation — all idempotent.
- Update
PROGRESS.mdwhen you ship something. Future-you (and future-Claude) will thank you.
What this walkthrough surfaced about the encyclopedia
A case study like this is also a quality test. The result: every concept this project needed has an encyclopedia entry. No glaring gaps surfaced during the walkthrough. The 16 sections + 12 how-tos + glossary cover the full journey.
The walkthrough also confirms that the cross-linking pattern works — almost every paragraph above could link to 3-5 entries, and they all exist as 🟩 COMPLETE.
If you’re reading this and a specific concept feels under-explained when you click through — that’s the gap worth filling, and worth telling Claude about so the entry can be improved.
Where to go from here
- Reading path: Build your first webapp 🟩 — the curated trail for someone starting their own version of this
- Reading path: Make my app secure 🟩 — the audit pass before going public
- Reading path: Understand the deploy pipeline 🟩 — what “git push” actually triggers
- Reading path: Master Claude Code 🟩 — get fluent at the tool that built all this
- The encyclopedia itself — every entry referenced above is one click away
Sources
- St Mark’s Bible Quest live site: https://stmarkbible.com
- In-repo
PROGRESS.mdandAGENT_GUIDE.mdatC:\Users\georg\st-marks-bible-quest— authoritative source of project history - Memory files:
project_progress_log.md,project_tech_stack.md,project_bible_study_app.md— the chronological log this walkthrough was built from