George Powell 4a53e09ab3 documentation
2026-07-07 17:49:33 -04:00
2026-02-02 02:52:53 -05:00
2025-12-19 03:36:59 -05:00
2026-03-25 01:39:24 -04:00
2026-02-26 00:51:48 -05:00
2026-07-07 17:06:12 -04:00
2026-07-07 17:02:24 -04:00
2026-03-22 00:58:17 -04:00
2025-12-16 10:23:24 -05:00
2026-03-25 01:39:24 -04:00
2026-02-24 19:10:53 -05:00
2025-12-16 10:23:24 -05:00
2026-02-02 02:52:53 -05:00
2026-03-25 01:39:24 -04:00
2026-07-07 17:49:33 -04:00
2026-07-07 17:49:33 -04:00
2026-03-25 09:06:21 -04:00
2025-12-16 10:23:24 -05:00

Bibdle

A daily Bible verse guessing game. Each day a verse is shown and players try to identify which of the 66 books of the Bible it comes from, receiving Wordle-style feedback (Testament match, Section match, Adjacent book, First letter, etc.) after each guess. A new verse is generated daily and progress is tracked across timezones.

Live at bibdle.com.

Tech Stack

  • Framework: SvelteKit 2 with Svelte 5 (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
  • ML (currently disabled): @xenova/transformers verse embeddings for a similarity search route
  • Deployment: @sveltejs/adapter-node, run under Bun, managed by a systemd service (see bibdle.service)

Getting Started

bun install

# Start the dev server (Vite, Bun runtime)
bun run dev

# Type checking
bun run check

# Tests (Bun test)
bun test
bun test tests/timezone-handling.test.ts   # single file
bun test --watch

# Production build & preview
bun run build
bun run preview

Database

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

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)

Architecture

Database Schema (src/lib/server/db/schema.ts)

  • userid, firstName, lastName, email (unique), passwordHash, appleId (unique), googleId (unique), isPrivate
  • sessionid (SHA-256 hash of token), userId (FK), expiresAt
  • dailyVerses — cached daily verse: date (unique), bookId, verseText, reference, createdAt
  • dailyCompletions — one row per player/date: anonymousId, date, guessCount, guesses (JSON of book IDs, nullable), completedAt. Unique on (anonymousId, date) to prevent duplicate submissions.
  • verseSubmissions — a log of community-submitted future verses: id, userId (FK → user.id ON DELETE SET NULL), scheduledDate (unique YYYY-MM-DD matching a dailyVerses row), selectedBookId, selectedChapter, selectedVerse (the anchor the user picked), submittedAt (server UTC millis, drives the 7-day cooldown). Indexed on userId (cooldown lookup) and scheduledDate (unique). The canonical verse text/reference lives on dailyVerses, joined by scheduledDate.

Sessions expire after 30 days and auto-renew when fewer than 15 days remain.

Bible Data (src/lib/types/bible.ts)

The bibleBooks array lists all 66 books with metadata:

  • testament: old | new
  • section: Law, History, Wisdom, Major Prophets, Minor Prophets, Gospels, Pauline Epistles, General Epistles, Apocalyptic
  • order (166, used for adjacency detection)

Game Logic

  • src/lib/utils/game.tsevaluateGuess() compares a guess to the target book and returns testamentMatch, sectionMatch, adjacent, and firstLetterMatch flags. getGrade() maps guess count to a letter grade (S+ down to C). Includes a special-case so that numbered Epistles (e.g. "1 John") match on first letter against any other numbered Epistle.
  • src/lib/stores/game-persistence.svelte.ts — reactive store that keeps guesses and per-day flags in sync with localStorage, keyed by date (bibdle-guesses-${date}). Resolves the player identity (logged-in user ID, or a locally generated anonymous UUID) and restores state.
  • src/lib/utils/share.ts — generates the share grid and text. Hint emojis: exact · 🟩 section · 🟧 testament · ‼️ adjacent · 🟥 no match.

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 and stores it permanently. The XML Bible is read and parsed in src/lib/server/xml-bible.ts; src/lib/server/bible-api.ts wraps it to produce a verse with a validated bookId, reference, and verseText.

Community Verse Submissions

Authenticated users who have solved today's puzzle can submit a verse for scheduling as a future "verse of the day." Selection is dropdown-based (cascading Book → Chapter → Verse selects), so content is always canonical NKJV text — no free-text entry. Each submission immediately reserves a concrete future date.

  • src/lib/server/verse-submission.ts — the 3-verse window composer (composeVerseWindow, fall-forward, never crosses a book), formatWindowReference (same-chapter hyphen / cross-chapter en-dash), and the assign-at-submit scheduling scan (empty / no-back-to-back-with-committed-neighbors / 60-day-repeat rules, transaction + retry-on-unique-conflict).
  • getVerseForDate(date) serves pre-written submission rows unchanged on their day; gap days (no committed row) are filled lazily by the random path, now extended to avoid the book of any committed neighbor (D-1 / D+1).
  • Rate limiting: one submission per user per rolling 7×24h window, measured in server UTC (not gameable). The win-screen button is always visible; it is greyed out with a countdown timer while the cooldown is active.
  • Attribution is anonymous everywhere except the admin-only /scheduled-verses view (the single exception, gated to ADMIN_EMAIL in src/lib/server/admin.ts), which surfaces submitter email for moderation.

Authentication (src/lib/server/auth.ts)

  • Token: base64url-encoded random bytes; stored as a SHA-256 hash in the DB. Cookie name: auth-session.
  • Anonymous users are identified by a client-generated UUID in localStorage. On sign-up, migrateAnonymousStats() re-attributes the user's dailyCompletions rows from the anonymous ID to the new user ID (duplicates for overlapping dates are dropped).
  • Apple Sign-In (src/lib/server/apple-auth.ts) and Google Sign-In (src/lib/server/google-auth.ts) are supported; the SvelteKit CSRF config trusts https://appleid.apple.com for the cross-origin form_post callback.

Routes

Pages

Route Description
/ Main game (classic mode). Verse display, search input, guesses table, win screen, streak/percentile. +page.ts sets ssr = false so the load runs client-side with the true local date, then POSTs that date to /api/daily-verse.
/imposter Imposter Mode — four verses are shown; three come from one book and one from a different book. Pick the one that doesn't belong.
/random Debug page showing a random verse from the NKJV.
/greek-random Debug page showing a random verse in parallel Greek (1904) / English (NKJV).
/similarity Search a sentence and return the most similar Bible verses via the (currently disabled) embeddings model.
/about About page with the project's backstory and social links.
/global Public stats dashboard: completions today/all-time, unique & weekly & monthly players, active streak distribution, 14-day completions trend, retention/return-rate metrics.
/progress Personal progress page (requires auth): activity calendar, 66-book grid with mastery tiers, insights, and achievements/milestones.
/stats Personal stats page (requires auth); returns requiresAuth: true for unauthenticated visitors and renders a sign-in modal.
/dev Local-time / countdown debug page.
/scheduled-verses Admin-only (gated to ADMIN_EMAIL in src/lib/server/admin.ts). Full historical + future log of verseSubmissions joined to dailyVerses and user, sorted by scheduled date. The sole surface where submitter identity is shown. Not linked from the UI.

API Endpoints

Endpoint Description
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, and percentile. Unique on (anonymousId, date).
GET /api/streak?anonymousId=X&localDate=Y Current streak: walks backwards from the client's local date through completed dates, counting consecutive days. Single-day streaks are reported as 0 (minimum displayed streak is 2).
GET /api/streak-percentile?streak=N&localDate=Y Streak percentile ranking computed across all players' current streaks.
GET /api/stats Aggregated stats used by the /global dashboard.
GET /api/imposter Generates 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.
POST /api/submit-verse Auth required. Accept { bookId, chapter, verse, localDate }; runs the solved-today gate, 7-day cooldown, structural validation, and the assign-at-submit scheduling scan; returns { scheduledDate, reference, windowText }.
GET /api/submit-verse/status?localDate=YYYY-MM-DD Auth required. Win-screen button state: canSubmit, cooldownEndsAt, lastSubmission, and this user's not-yet-reached upcoming submissions.
GET /api/bible/structure Public. 66-book verse counts per chapter (cascading-dropdown payload, cached indefinitely).
GET /api/verse-window?bookId=gen&chapter=1&verse=1 Public. Live 3-verse preview (fall-forward window) for the submit selector.

Other Endpoints

  • GET /feed.xml — RSS feed of daily verses.
  • GET /sitemap.xml — XML sitemap for SEO.

Critical: Date/Time Handling

Bibdle is played across many timezones. The verse shown must always be the verse for the calendar date at the player's location — never the server's timezone, never UTC.

  • The client computes today's date with new Date().toLocaleDateString("en-CA") (YYYY-MM-DD) and sends it to the server.
  • Server-side date arithmetic always uses UTC methods on the client-provided date string (new Date(dateStr + 'T00:00:00Z') + setUTCDate/getUTCDate) to avoid timezone drift — see /api/streak and /api/send-daily-verse.
  • / has ssr = false so the load runs client-side with the real local date.
  • The main page also reloads itself if the tab regains focus on a new calendar day.
  • The user's local date is never placed in the URL; it is only ever sent to API routes.

Streak Calculation

A streak counts consecutive calendar days (in the player's local timezone) on which the puzzle was completed:

  • The client passes localDate; the server never uses its own clock.
  • /api/streak walks backwards from localDate through dailyCompletions, counting each completed day; stops at the first missing day. It's called from the win screen, so today is typically completed. Single-day streaks are reported as 0 — the minimum displayed streak is 2.
  • /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.

Key Files

File Purpose
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/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 (set-completion, streak, etc.)
src/lib/server/admin.ts ADMIN_EMAIL constant for the /scheduled-verses admin route.
src/lib/server/verse-submission.ts Window composer + scheduling scan for community verse submissions.
src/lib/components/SubmitVerse.svelte Win-screen submit button (logged-out sign-in dropdown / cooldown + countdown / cascading selects + preview + submit).
src/lib/types/bible.ts 66-book metadata and TypeScript types
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/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

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.

Background

Bibdle was created as a small, daily nudge to read the Bible — inspired by the Wordle story of a personal project that grew organically through word of mouth. The full backstory lives on the /about page.

S
Description
No description provided
Readme 22 MiB
Languages
TypeScript 56.7%
Svelte 41.6%
Shell 1.2%
CSS 0.3%
JavaScript 0.1%