Files
bibdle/CLAUDE.md
T
2026-07-07 12:13:35 -04:00

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 with fast-xml-parser; a Greek 1904 and Swedish 2000 translation are also bundled for alternate modes
  • Deployment: @sveltejs/adapter-node run under Bun, managed by a systemd service (bibdle.service)
  • ML (currently disabled): @xenova/transformers verse 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.ts has ssr = false so 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/streak walks backwards from localDate through the dailyCompletions records, 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/streak returns 0 for 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'), then setUTCDate/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.idcreateUser() 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 | new
  • section: Law, History, Wisdom, Major Prophets, Minor Prophets, Gospels, Pauline Epistles, General Epistles, Apocalyptic
  • order (1-66, used for adjacency detection)

Daily Verse System

src/lib/server/daily-verse.tsgetVerseForDate(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:

  • guesses array stored in localStorage keyed by date: bibdle-guesses-${date}
  • Each Guess tracks: book, testamentMatch, sectionMatch, adjacent, firstLetterMatch
  • evaluateGuess() includes a special case: numbered Epistles (e.g. "1 John") match on first letter against any other numbered Epistle
  • isWon derived from whether any guess matches the correct book
  • getGrade() 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 via migrateAnonymousStats() (re-attributes dailyCompletions rows 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, appleId field), and Google Sign-In (src/lib/server/google-auth.ts, googleId field). The SvelteKit CSRF config trusts https://appleid.apple.com for the cross-origin form_post callback.

Stats & Streak (src/routes/stats/, src/routes/progress/)

  • /stats and /progress require auth; the server load returns requiresAuth: true for 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 localDate through dailyCompletions; stops at the first missing day. Single-day streaks are reported as 0 — the minimum displayed streak is 2.
  • /api/streak walks from localDate only (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 given YYYY-MM-DD date
  • POST /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 player
  • GET /api/streak-percentile?streak=N&localDate=Y — Streak percentile ranking across all players
  • GET /api/stats — Aggregated stats for the /global dashboard
  • GET /api/imposter — Generate a four-verse imposter-mode round
  • POST /api/similar-verses — Semantic verse search via embeddings
  • POST /api/send-daily-verse — Cron-only (bearer CRON_SECRET); posts today's verse to the Discord webhook
  • POST /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 logic
  • src/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 injection
  • src/routes/imposter/, /random, /greek-random, /similarity — Alternate game/debug modes
  • src/routes/about/, /global/, /progress/, /stats/, /dev/ — Supporting pages
  • src/lib/server/auth.ts — Session management, password hashing, anonymous→user migration
  • src/lib/server/apple-auth.ts, google-auth.ts — OAuth providers
  • src/lib/server/daily-verse.ts — Per-date verse caching/lookup
  • src/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 parser
  • src/lib/server/bible.ts — Bible book utility functions
  • src/lib/server/milestones.ts — Achievement/milestone calculation
  • src/lib/utils/game.ts — Guess evaluation and grading
  • src/lib/utils/share.ts — Share grid/text generation
  • src/lib/utils/streak.ts, stats-client.ts, stats.ts — Client-side streak/stats fetching and formatting
  • src/lib/stores/game-persistence.svelte.ts — Reactive localStorage-backed game state
  • src/lib/types/bible.ts — 66-book metadata and TypeScript types
  • src/lib/server/db/schema.ts — Drizzle ORM schema
  • src/hooks.server.ts — Session validation hook; (commented-out) embeddings init
  • tests/ — 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 URL
  • CRON_SECRET — Bearer token protecting the cron-only /api/send-daily-verse endpoint
  • DISCORD_DAILY_WEBHOOK — Webhook for posting the daily verse to Discord
  • AUTH_SECRET — Secret for Apple Sign-In
  • APPLE_ID / APPLE_TEAM_ID / APPLE_KEY_ID / APPLE_PRIVATE_KEY — Apple Sign-In credentials
  • SMTP_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.