12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Bibdle is a daily Bible verse guessing game built with SvelteKit 2 / Svelte 5. Players read a verse and try to guess which book of the Bible it comes from. The game provides feedback hints (Testament match, Section match, Adjacent book, First letter, etc.) similar to Wordle-style games. Progress is stored locally in the browser and a new verse is generated daily.
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
(Make sure you use the Svelte agent to execute these commands)
Available MCP Tools:
1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections. After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions. You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
Tech Stack
- Framework: SvelteKit 2 with Svelte 5 (uses runes:
$state,$derived,$effect,$props) - Styling: Tailwind CSS 4
- Database: SQLite (
bun:sqlite) with Drizzle ORM - Auth: Session-based — email/password (argon2id via
Bun.password), plus Apple and Google OAuth - Bible text: Local NKJV XML (
EnglishNKJBible.xml), parsed withfast-xml-parser; a Greek 1904 and Swedish 2000 translation are also bundled for alternate modes - Deployment:
@sveltejs/adapter-noderun under Bun, managed by a systemd service (bibdle.service) - ML (currently disabled):
@xenova/transformersverse embeddings for a similarity search route
The package version is 3.0.0alpha.
Development Commands
# Start development server
bun run dev
# Type checking
bun run check
bun run check:watch
# Run tests
bun test
bun test --watch
bun test tests/timezone-handling.test.ts # Run a single test file
# Build for production
bun run build
# Preview production build
bun run preview
# Database operations
bun run db:push # Push schema changes directly (avoid in prod)
bun run db:generate # Generate migrations
bun run db:migrate # Run migrations
bun run db:studio # Open Drizzle Studio GUI
Critical: Date/Time Handling
Bibdle is played by users across many timezones worldwide. The verse shown to a player must always be the verse for the calendar date at their location — not the server's timezone, not UTC. A user in Tokyo on Wednesday must see Wednesday's verse, even if the server (or a user in New York) is still on Tuesday.
NEVER use server time or UTC time for user-facing date calculations.
- Get today's date client-side:
new Date().toLocaleDateString("en-CA")→YYYY-MM-DD - Pass the date to the server as a query param or POST body (
localDate) - Server-side date arithmetic must use UTC methods on the client-provided date string:
new Date(dateStr + 'T00:00:00Z')+setUTCDate/getUTCDate src/routes/+page.tshasssr = falseso the load runs client-side with the true local date- Never set the user-facing URL to include their date as a parameter. It should always be passed to an API route behind the scenes if needed.
Streak Calculation
A streak counts consecutive calendar days (in the user's local timezone) on which the user completed the puzzle. The rules:
- The client passes its local date (
localDate) to the streak API. The server never uses its own clock. /api/streakwalks backwards fromlocalDatethrough thedailyCompletionsrecords, counting each completed day. It stops at the first missing day. It's called from the win screen, so today is typically completed./api/streak-percentile(which ranks all players) anchors on today-if-played-else-yesterday, so a player's streak isn't zeroed mid-day before they've had a chance to complete today's puzzle.- A streak of 1 (completed only today, with no prior consecutive days) is not displayed —
/api/streakreturns0for any streak < 2, and the minimum shown streak is 2. - All date arithmetic on the server must use UTC methods on the client-provided date string to avoid timezone drift:
new Date(localDate + 'T00:00:00Z'), thensetUTCDate/getUTCDate.
Architecture
Database Schema (src/lib/server/db/schema.ts)
- user:
id,firstName,lastName,email(unique),passwordHash,appleId(unique),googleId(unique),isPrivate - session:
id(SHA-256 hash of token),userId(FK),expiresAt - dailyVerses (table
daily_verses): cached daily verse —date(unique),bookId,verseText,reference,createdAt - dailyCompletions (table
daily_completions): one row per player/date —anonymousId,date,guessCount,guesses(JSON array of book IDs; nullable),completedAt. Unique on(anonymousId, date)to prevent duplicate submissions.
Sessions expire after 30 days and auto-renew when < 15 days remain.
Identity model: logged-in users' anonymousId is their user.id — createUser() inserts the user with id = anonymousId so existing stats carry over. Anonymous users get a client-generated UUID stored in localStorage (bibdle-anonymous-id).
Bible Data (src/lib/types/bible.ts)
The bibleBooks array contains all 66 Bible books with metadata:
testament:old|newsection:Law,History,Wisdom,Major Prophets,Minor Prophets,Gospels,Pauline Epistles,General Epistles,Apocalypticorder(1-66, used for adjacency detection)
Daily Verse System
src/lib/server/daily-verse.ts → getVerseForDate(date): returns the cached verse for a date if present, otherwise fetches a random verse from the local XML Bible (src/lib/server/xml-bible.ts, wrapped by src/lib/server/bible-api.ts) and stores it permanently. The client calls POST /api/daily-verse with its local date; src/routes/+page.server.ts only loads user/session (the verse is fetched client-side because +page.ts sets ssr = false).
Game Logic
Core logic lives in src/lib/utils/game.ts and the reactive store src/lib/stores/game-persistence.svelte.ts; src/routes/+page.svelte wires them into the UI.
State Management:
guessesarray stored inlocalStoragekeyed by date:bibdle-guesses-${date}- Each
Guesstracks:book,testamentMatch,sectionMatch,adjacent,firstLetterMatch evaluateGuess()includes a special case: numbered Epistles (e.g. "1 John") match on first letter against any other numbered EpistleisWonderived from whether any guess matches the correct bookgetGrade()maps guess count to a letter grade (S+ → C)
Hint System, for share grid:
- ✅ Exact match | 🟩 Section match | 🟧 Testament match | ‼️ Adjacent book | 🟥 No match
Authentication System (src/lib/server/auth.ts)
- Token generation: base64url-encoded random bytes; stored as SHA-256 hash in DB. Cookie name:
auth-session. - Anonymous users: identified by a client-generated UUID in
localStorage; stats migrate on sign-up viamigrateAnonymousStats()(re-attributesdailyCompletionsrows from the anonymous ID to the new user ID; overlapping dates are dropped). - Three sign-in methods: email/password (argon2id via
Bun.password), Apple Sign-In (src/lib/server/apple-auth.ts,appleIdfield), and Google Sign-In (src/lib/server/google-auth.ts,googleIdfield). The SvelteKit CSRF config trustshttps://appleid.apple.comfor the cross-originform_postcallback.
Stats & Streak (src/routes/stats/, src/routes/progress/)
/statsand/progressrequire auth; the server load returnsrequiresAuth: truefor unauthenticated visitors, and the page renders a sign-in modal.- The current streak is fetched from the server via
GET /api/streak?anonymousId=X&localDate=Y(the server never uses its own clock). - Streak walk-back: counts consecutive days backwards from
localDatethroughdailyCompletions; stops at the first missing day. Single-day streaks are reported as0— the minimum displayed streak is 2. /api/streakwalks fromlocalDateonly (it's called after a win, so today is completed)./api/streak-percentile, which ranks all players, anchors on today-if-played-else-yesterday so mid-day streaks aren't zeroed.- Achievements/milestones are computed server-side in
src/lib/server/milestones.ts.
API Endpoints
POST /api/daily-verse— Fetch (and cache) the verse for a givenYYYY-MM-DDdatePOST /api/submit-completion— Submit a game result (anonymousId,date,guessCount,guesses); returns solve rank, guess rank, total solves, average guesses, ties, percentile. Unique on(anonymousId, date).GET /api/streak?anonymousId=X&localDate=Y— Current streak for a playerGET /api/streak-percentile?streak=N&localDate=Y— Streak percentile ranking across all playersGET /api/stats— Aggregated stats for the/globaldashboardGET /api/imposter— Generate a four-verse imposter-mode roundPOST /api/similar-verses— Semantic verse search via embeddingsPOST /api/send-daily-verse— Cron-only (bearerCRON_SECRET); posts today's verse to the Discord webhookPOST /api/dev/seed-history— Dev seeding helper
Other endpoints: GET /feed.xml (RSS) and GET /sitemap.xml (SEO).
Key Files
src/routes/+page.svelte— Main game UI and client-side logicsrc/routes/+page.server.ts/+page.ts— Server load (user/session) + client load (ssr: false, fetches the daily verse)src/routes/+layout.svelte— App shell, title animation, theme toggle, analytics injectionsrc/routes/imposter/,/random,/greek-random,/similarity— Alternate game/debug modessrc/routes/about/,/global/,/progress/,/stats/,/dev/— Supporting pagessrc/lib/server/auth.ts— Session management, password hashing, anonymous→user migrationsrc/lib/server/apple-auth.ts,google-auth.ts— OAuth providerssrc/lib/server/daily-verse.ts— Per-date verse caching/lookupsrc/lib/server/xml-bible.ts— Local XML Bible parsing (NKJV / Greek / Swedish)src/lib/server/bible-api.ts— Random verse fetching on top of the XML parsersrc/lib/server/bible.ts— Bible book utility functionssrc/lib/server/milestones.ts— Achievement/milestone calculationsrc/lib/utils/game.ts— Guess evaluation and gradingsrc/lib/utils/share.ts— Share grid/text generationsrc/lib/utils/streak.ts,stats-client.ts,stats.ts— Client-side streak/stats fetching and formattingsrc/lib/stores/game-persistence.svelte.ts— Reactive localStorage-backed game statesrc/lib/types/bible.ts— 66-book metadata and TypeScript typessrc/lib/server/db/schema.ts— Drizzle ORM schemasrc/hooks.server.ts— Session validation hook; (commented-out) embeddings inittests/— Bun test suites: timezone, game, bible, stats, share, sign-in migration
Environment Variables
See .env.example. Required/used variables:
DATABASE_URL— Path to the SQLite database file (e.g.,prod.db)PUBLIC_SITE_URL— Canonical site URLCRON_SECRET— Bearer token protecting the cron-only/api/send-daily-verseendpointDISCORD_DAILY_WEBHOOK— Webhook for posting the daily verse to DiscordAUTH_SECRET— Secret for Apple Sign-InAPPLE_ID/APPLE_TEAM_ID/APPLE_KEY_ID/APPLE_PRIVATE_KEY— Apple Sign-In credentialsSMTP_USERNAME/SMTP_TOKEN/SMTP_SERVER/SMTP_PORT— Email (nodemailer)
Deployment
Production uses @sveltejs/adapter-node run under Bun via a systemd service. deploy.sh pulls latest, installs deps, builds, and restarts bibdle.service (which runs bun --bun build/index.js on port 5173 with DATABASE_URL=prod.db). See bibdle.service for the unit file.
A Note
The main developer of this project is still learning a lot about developing full-stack applications. If they ask you to do something, make sure they understand how it will be implemented before proceeding.