# 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 ```bash # 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.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` | `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.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:** - `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.