14 KiB
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 withfast-xml-parser; a Greek 1904 and Swedish 2000 translation are also bundled for alternate modes - ML (currently disabled):
@xenova/transformersverse embeddings for a similarity search route - Deployment:
@sveltejs/adapter-node, run under Bun, managed by a systemd service (seebibdle.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 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)
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 — 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.idON DELETE SET NULL),scheduledDate(uniqueYYYY-MM-DDmatching adailyVersesrow),selectedBookId,selectedChapter,selectedVerse(the anchor the user picked),submittedAt(server UTC millis, drives the 7-day cooldown). Indexed onuserId(cooldown lookup) andscheduledDate(unique). The canonical verse text/reference lives ondailyVerses, joined byscheduledDate.
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|newsection:Law,History,Wisdom,Major Prophets,Minor Prophets,Gospels,Pauline Epistles,General Epistles,Apocalypticorder(1–66, used for adjacency detection)
Game Logic
src/lib/utils/game.ts—evaluateGuess()compares a guess to the target book and returnstestamentMatch,sectionMatch,adjacent, andfirstLetterMatchflags.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 withlocalStorage, 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.ts → getVerseForDate(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-versesview (the single exception, gated toADMIN_EMAILinsrc/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'sdailyCompletionsrows 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 trustshttps://appleid.apple.comfor the cross-originform_postcallback.
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/streakand/api/send-daily-verse. /hasssr = falseso 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/streakwalks backwards fromlocalDatethroughdailyCompletions, 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 as0— 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.