What we’ve shipped
Every change to Selah, newest first, written for the people who use it rather than the people who built it. Last shipped .
The companion now reaches out to you.
Heartbeat Companion Nudges + Nightly Learning Loop
The companion now reaches out to you.
Every 15 minutes, a Vercel Cron evaluates your recent notes, prayers, and captures with Claude Haiku. If there's something worth surfacing — a stale note, a forgotten prayer, a connection to the week's devotional theme — it sends you a Telegram message. Feels like a thoughtful friend, not a notification bot.
Smart nudge timing.
You control active hours (default 8am–10pm) in Settings. Nudges respect a daily cap of 2 so they stay meaningful. Overnight windows work correctly — set 10pm–6am if you want late-night prompts.
Nudges that don't repeat.
The system reads the last 7 days of nudges before asking the LLM to generate a new one. If you've already been reminded about Romans 8 three times this week, it won't ask again.
Heartbeat settings in the web app.
The Settings page now has a Proactive Check-ins card. Toggle nudges on/off, set your active hours, and write custom instructions for the companion (e.g., "Only nudge me about Study-stage notes" or "Focus on prayer when I haven't prayed in 3 days").
Nightly companion learning loop.
Every night at 2am, the companion reviews the past 24 hours of your activity — notes edited, captures saved, prayers engaged — and updates its understanding of you. Five facts about your patterns and interests stay current. Your companion profile is regenerated from those facts so every conversation starts with fresh context.
Activity-aware learning.
The learning loop uses Haiku to reason over your recent activity digest. It can add new facts, update existing ones, and remove stale ones — so the companion's knowledge of you actually evolves rather than accumulating noise.
Telegram formatting fixed.
Companion responses now render bold, italic, inline code, and links correctly in Telegram (HTML mode). This was a regression introduced in the prior release and is now fully restored.
Added
`src/lib/heartbeat.
ts
— context assembly, LLM evaluation,parseHeartbeatResponse,buildHeartbeatSystemPrompt``src/app/api/cron/heartbeat/route.
ts
— 15-minute Vercel Cron (GET),isWithinActiveHours,isNudgeCapReached``src/app/api/cron/companion-learn/route.
ts` — nightly learning cron (GET), activity digest, fact maintenance
`src/app/api/heartbeat-preferences/route.
ts` — GET/PUT/PATCH heartbeat preferences
`src/app/settings/heartbeat-card.
tsx` — Proactive Check-ins settings card
`src/lib/companion-learning.
ts
—buildActivityDigest,runLearningLoop`, fact CRUD helpers`src/lib/telegram.
ts
—sendHeartbeatNudge,markdownToTelegramHtml` (restored)`drizzle/0015_heartbeat.
sql
—heartbeat_nudgestable +companion_preferences` heartbeat columns`drizzle/0016_companion_learning.
sql
—topFactscolumn oncompanion_preferences`- 5 new test files: 432 tests total (up from ~370)
Interview notes now save as full, editable study documents.
Full Interview Notes + Cross-Surface Session Navigation
Interview notes now save as full, editable study documents.
When you say "save this" on Telegram or in the web companion at the end of an interview, the companion captures everything: the theological arc of the conversation (as a pull quote), all your significant insights (no 3-6 limit, no truncation), scripture references with context, your most powerful verbatim quotes, and any open questions to explore later. The saved note opens as a rich, editable TipTap document — not a read-only summary. Old notes keep working through a backward-compat read path.
Tap "View Note" from Telegram.
After saving an interview, prep, or general note from Telegram, the companion now includes an inline "View Note" button that deep-links straight to the editable note in the web app. No more "I can't edit notes" confusion.
`/sessions` on Telegram.
See your recent conversations across both web and Telegram with one command. Tap any session to jump to it on the web. Same agent, multiple surfaces — the session is unified.
"History" button on the web companion.
The clock icon now has a visible "History" label so you can actually find the session drawer. The drawer itself already listed topics + dates; now new users know it exists.
Deep-link into any past session.
Navigate to
/companion?sessionId=Xand the companion loads that conversation directly. If the session doesn't exist (typo, deleted, stale Telegram link), you get a clear error banner — "Couldn't find that conversation. Started a new chat." — not a silent empty chat.Notes link back to their source conversation.
The "View companion interview" button on an interview note now routes through the unified session URL, so you can jump from the saved note back to the conversation that produced it, continue thinking, and save again.
Telegram companion loop crash fixed.
markdownToTiptap(undefined)no longer throws — empty input returns a valid empty-paragraph doc.parseLLMJsonnow recovers when the LLM wraps JSON output in prose. Both fixed the "Something went wrong" errors that were locking users out of their own conversations.System prompt truth-up.
The companion no longer believes falsehoods about its own capabilities. It knows notes are editable at
/notes/[id], it knows it can save verbatim quoted content, and it knows to capture all insights without truncating for a 3-6 bullet limit.Data integrity: `openQuestions` field drop fixed.
Interview notes stored questions under the wrong field name, which caused every open-question section to render blank. Fixed with a dual-field read in the notes editor and the embeddings backfill cron — old notes now display their questions correctly, and re-embeddings don't silently lose questions text.
Prayer-Aware Companion
`/prayers` — your prayer board in one tap.
Send
/prayersin Telegram to see all your active, ongoing, and waiting prayers grouped by status. Each prayer gets inline [Answered] / [Waiting] / [Ongoing] buttons. Tapping a button updates the status in-place — the message edits itself and the buttons disappear so you can't accidentally retap.`/note [text]` — save a note directly from Telegram.
Send
/note Romans 5:3 — suffering produces perseveranceand it goes straight to your notes library with an auto-title and a deep link back to the note in the app.Companion knows your prayer board.
Every conversation now includes your most recent non-answered prayers as context. When you say "God answered my prayer about the job offer," the companion understands what you mean and can call
update_prayer_statusto mark it answered. It won't guess UUIDs — if it's unsure which prayer you mean, it asks by title.Three new companion tools:
read_prayers(filtered board lookup),update_prayer_status(mark answered/waiting/ongoing with an optional note),create_note(save a theological insight or study reflection directly from conversation).Bug fix:
/start,/help,/recent,/profile,/prayers, and/noteno longer send a redundant 👀 acknowledgment reaction — the reaction from the message handler is sufficient.
End-to-end devotional pipeline.
Adaptive Daily Devotional System
End-to-end devotional pipeline.
Users receive a fresh scripture-based devotional every day at their chosen local hour, delivered via Telegram and/or surfaced in the companion chat. The system is week-aware: Monday opens a theme, Friday closes with reflection, mid-week entries develop it.
`devotional_weeks` + `devotional_entries` tables
(migration
0011) — week records track theme, proposedNextTheme, and week-end reflection; entry records carry all rendered fields (dayRole, scriptureRef, scriptureText, opening, reflection, prayerPrompt, question) plus Telegram delivery metadata.`src/lib/devotional.ts`
—
ensureTodaysDevotionalis the core coordinator: claim a pending slot, call the role-specific LLM tool (theme_open/reflection_close/middle), write the result. Idempotent — concurrent callers compete for apendingrow; only the first writer wins. LLM output truncated viav100()/txt()helpers before every DB write.tool_choice: { type: "tool", name }forces the correct role-specific tool to fire.`POST /api/cron/devotional`
— hourly Vercel cron that sweeps stale pending entries (> 10 min old), finds users at their delivery hour, generates entries, and sends to Telegram. Midnight hour normalized (
% 24) for V8Intl.DateTimeFormatquirk. Query capped at 500 users per run with a log warning when the limit is hit.`POST /api/cron/devotional-extract`
— nightly cron that extracts week-level reflection from completed entries and proposes next week's theme.
`GET /api/devotional`
— returns today's devotional for the authenticated user (generating on first request).
`PATCH /api/devotional-preferences`
— saves timezone, delivery hour, and
devotional_enabledflag. IANA timezone validated viaIntl.DateTimeFormat;isValidIanaZoneexported for testability.DevotionalMessage UI
(
src/components/companion/DevotionalMessage.tsx) — inline companion card rendering scripture reference, opening, reflection, prayer prompt, and reflection question with gold-tinted scripture styling per DESIGN.md.DevotionalCard settings
(
src/components/settings/DevotionalCard.tsx) — timezone picker (20 common zones), hourly delivery selector, enable toggle, "Send now" preview button.Daily Rhythm onboarding step
— new step 4 in the onboarding flow introducing the devotional, with a scripted companion demo that walks through the companion philosophy using pre-scripted message pairs.
Notes page badge
—
devotionalnote type now renders a gold "Devotional" badge in the notes list.`isDevotionalStructured` type guard
— replaces a blind cast in the companion page; validates
dayRoleshape before rendering the devotional card inline.Telegram delivery
—
sendDevotionalrenders devotionals as clean Markdown messages; atomic update setsdeliveredTelegramAtonly whenIS NULLto prevent double-sends.15 new pure-function tests
(269 total) —
chunkMessage,verifyWebhookSecret,isValidIanaZonewith full branch coverage.Four adversarial-review fixes:
midnight normalization, scale cap log warning,
reflection_closeidempotency guard,handleConnectsingle body parse.
New user discovers the bot → signup → back to Telegram.
Telegram Signup (Phase 1)
New user discovers the bot → signup → back to Telegram.
Previously, a Telegram user with no Selah account hit a dead end: the bot sent them to
/settings/telegram(7 steps to actually get connected). Now the bot creates a one-time signup invite, sends a single "Sign up to Selah" button, the website binds the chat on signup, and the user lands back in Telegram with the bot already greeting them.`telegram_signup_invites` table
(migration
0010) — 32-byte url-safe random token, 24h TTL, single-use, tracks chatId + Telegram username/first name. Inverse direction of the existingtelegram_link_codesflow (website → bot).`src/lib/telegram-signup.ts`
—
createSignupInvite,findOpenInviteForChat,findSignupInvite,claimSignupInvite,pushTelegramWelcomeIfEligible,buildWelcomeMessage. The claim function handles happy path, invalid/expired/consumed tokens, and cross-user guard with idempotent self-reclaim.`POST /api/telegram/claim`
— auth-required. Body
{ token }. Binds the chat to the authenticated user and returnsbotDeepLinkfromTELEGRAM_BOT_USERNAME. Maps claim errors to400(invalid/expired/consumed) and409(chat_linked_to_another_user).Webhook unlinked-user branch
(
src/app/api/telegram/webhook/route.ts) — replaced the dead-end "go to settings" message with a personalized welcome + "Sign up to Selah" button. Reuses an existing open invite if the user re-messages instead of creating a fresh row each time.Sign-up + sign-in pages
(
src/app/sign-up/page.tsx,src/app/sign-in/page.tsx) — read?via=telegram&t=TOKENon mount, stash the token insessionStorage, show a "Signing up from Telegram" banner. After successful auth, call/api/telegram/claimimmediately. Sign-up routes to/onboarding?via=telegram; sign-in (already-onboarded users) redirects straight to the bot viawindow.location. Wrapped inSuspenseforuseSearchParams()per Next.js 16.Onboarding flow
(
src/app/onboarding/onboarding-flow.tsx) — detectsvia=telegramfrom the URL, hides the "How did you find Selah?" question, forcessignup_source = "telegram"in the PATCH (including on Skip), and swaps the step 3 CTA from "Enter Selah" to "Open Telegram." On completion, readstelegramBotDeepLinkfrom the response and redirects viawindow.location.`/api/onboarding` PATCH
now returns
telegramBotDeepLinkwhensignup_source === "telegram"andTELEGRAM_BOT_USERNAMEis set. Also triggerspushTelegramWelcomeIfEligibleon the first completion transition (gated behind!before.completed). Non-fatal if the push fails.Welcome push
— 3-tier context-aware greeting: anchors on first capture content if saved, else on current focus, else falls back to a generic "share a thought" prompt. All under 2 sentences per DESIGN.md's Telegram response rules. No emoji in message text (DESIGN.md rule).
Memory fact seeding
remains unchanged:
signup_source = "telegram"is analytics, not memory, so it isn't written tomemory_facts.Claim-immediately-after-signup architecture
— the chat → user link is established before onboarding begins. If the user abandons mid-onboarding, they can return to Telegram and keep using the bot; onboarding resumes on their next web visit.
Tests (38 new, 218 total):
20 helper tests (
telegram-signup.test.ts), 11 claim-route tests (telegram-claim-route.test.ts), plus updates to the onboarding-route suite covering the welcome-push gate and the bot deep link response shape.
Onboarding — Companion Context
Step 1 redesigned as a single dense screen
— role (required-ish) plus four optional companion-context questions: Bible translation, what they're studying right now, theological tradition, and how they found Selah. All optional fields can be skipped with one button.
Translation preference
— 9-option pill row (ESV, NIV, NLT, KJV, NASB, CSB, NRSV, MSG, Other), defaulted to ESV. Stored on
user_onboarding.translation.Tradition
— 11-option pill row (Reformed, Wesleyan, Pentecostal, Baptist, Anglican, Lutheran, Catholic, Orthodox, Non-denominational, Still exploring, Other). Framed sensitively: "so we don't push contradictory stuff." Stored on
user_onboarding.tradition.Current focus
— single-line text input. "What are you studying right now?" → free text up to 500 chars. The single biggest unlock for first-session value.
Signup attribution
— "How did you find Selah?" pill row (Friend, Social, Search, Blog/podcast, Other). Stored on
user_onboarding.signup_source. Analytics-only, NOT memory.Memory fact seeding on completion
— when a user finishes onboarding, role/translation/tradition/currentFocus are written into
memory_facts(category:life_context/preference/theology, confidence 0.95, sourceTypemanual). The companion sees these from message #1 instead of cold-starting from zero. Gated so re-entry doesn't double-seed; non-fatal if seeding fails.`PillRow` reusable component
inside the onboarding flow with proper
radiogroup/radioARIA roles, 44px min touch targets, sage active state.Migration `0009_user_onboarding_context.sql`
— additive
ALTER TABLEfor the four new columns. Idempotent.30 new tests added (180 total passing)
— helper coverage for the new fields, EAV mapping in seedMemoryFactsFromOnboarding, route validation for every new field's enum + bounds, the seeding gate (fires once, no re-fire on re-entry), and the non-fatal error path.
Signup + Onboarding
Dedicated `/sign-up` page
— New optimized signup form replaces the inline toggle on
/sign-in. Name is optional. Password has a show/hide toggle. Clear value prop, trust microcopy ("Free. No credit card. Takes 30 seconds."), and friendly error states.Sign-in focused
—
/sign-inis now sign-in only with a clear link to/sign-upfor new users. Matches signup-flow-cro best practices: one page, one job.New user onboarding flow
—
/onboardingis a 3-step guided experience new users hit immediately after signup:- Welcome + role — Pick how you use scripture (pastor, small group leader, student, personal). Skippable. Feeds companion personalization later.
- First capture (aha moment) — Guided textarea with an example placeholder. Saves a real stream capture so the user sees their Home populated immediately. Supports Cmd/Ctrl+Enter to submit.
- You're ready — Warm recap of what Selah can do (capture, companion, notes, prayer). Single CTA: "Enter Selah."
Onboarding state in DB
— New
user_onboardingtable tracks role, first capture, andcompleted_at. Home page redirects any user withcompleted_at IS NULLto/onboarding, so returning users who bailed mid-flow resume where they left off.`/api/onboarding` route
—
GETreads status (creates row lazily),PATCHupdates role, saves first capture, or marks complete. All fields optional so the client can update progressively.Proxy matcher update
—
/sign-upis now excluded from Neon Auth proxy alongside/sign-inso unauthenticated visitors can reach it.
Upload a profile photo
Profile
Upload a profile photo
— New photo section at the top of Settings → Public profile. Pick an image and it's resized, cropped to a square, and saved automatically. Change or remove anytime. JPG, PNG, and WebP welcome.
Branded default avatar
— Accounts without a photo now show a warm soft-gold mark with a sage circle in the middle, matching the design system. Same mark on shared notes, the settings page, and anywhere the avatar appears.
Avatar on the settings page
— Your avatar now sits beside your name and email in the Account card, and beside the Public profile link so it's obvious where to change it.
Consistent across surfaces
— Shared note pages, settings, and the profile editor all use the same Avatar component, so the default and uploaded states look identical everywhere.
Tables in the editor
Knowledge Graph
Editable concept summaries
— Click any summary on a concept's detail page to edit it inline. Click outside to auto-save. Write your own or hit the sparkle button to generate one with AI instantly (no waiting for background jobs).
"Generate Summary" button
— One click to create a summary from your linked notes, captures, and teachings using AI. Available on concepts with no summary and inside the editor. 300 character limit keeps summaries tight.
Fixed stuck "updating" concepts
— Concepts showing "updating" forever are now fixed. The background summarizer was being starved by the extraction pipeline. Summaries now process first, with a time budget that drains the full queue.
Smarter stale detection
— The companion no longer mass-flags short-named concepts like "God" or "Sin" for summary refresh. Stale matching uses in-memory lookup instead of per-fact database queries.
Scripture parsing fix
— "1 John 4:8", "2 John", and "3 John" references now parse correctly (trailing space in the regex key was eating the chapter separator).
Prayer Board
Inline-editable titles
— Click any prayer title to edit it in place. Auto-sizing textarea preserves line wrapping. Enter saves, Escape cancels, blur saves. Optimistic updates with rollback.
Cleaner card layout
— "Move to" buttons and delete icon collapse behind hover on desktop, always visible on mobile. Trash icon moved to top-right corner. Cards at rest show just the title and timestamp.
Loading skeleton
— Card-shaped placeholders while prayers load instead of a blank board.
Keyboard shortcut hint
—
Nbadge next to "Add Prayer" button on desktop.Serif page heading
— "Prayer Board" now uses Cormorant Garamond per the design system.
API: title updates
— PATCH endpoint now supports updating prayer titles alongside status changes.
Design System
Added **Keyboard Shortcut Hints** pattern to DESIGN.
md (styling, placement, rules).
Added **Loading Skeletons** pattern to DESIGN.
md (card skeletons, anti-spinner rule).
- New decisions logged: hover-only card actions, shortcut hints, skeleton convention.
Notes
Tables in the editor
— Type
/tableto insert a 3x3 table with a header row. Columns are resizable by dragging. Tables styled to match the warm journal aesthetic (sage selection highlights, warm-white headers).Paste markdown, get formatted content
— Paste a markdown table, heading, or list into the editor and it auto-converts to rich text. No more raw
|pipes orasterisks.Selection-based cleanup
— Select text in a note and click the sparkle (✦) in the floating toolbar to format just that section with AI. No selection = format the whole document (as before). Long notes no longer lose content during cleanup.
Slash commands fixed
— The
/menu now appears reliably when you type/at the start of a line.
Companion
Formatted interview transcripts
— AI and user messages in companion interviews now render bold, italic, lists, headings, blockquotes, and tables instead of showing raw markdown like
this.
Fixes
- Reformat no longer truncates notes over 8K characters (raised to 25K with clear error for very long notes).
- Reformat errors now show in the UI instead of silently failing.
- Table CSS aligned to design system (4px grid spacing, `tabular-nums`, accessible `scope="col"` headers).
- Defensive HTML escaping in companion transcript rendering.
`/prep` command
Companion
`/prep` command
— Type
/prep [topic]to start a Socratic interview for sermon, Bible study, or devotional prep. The AI interviews you (8+ exchanges before it lets you build), then formats your words into a structured teaching note with inline enrichment tags ([Historical],[Greek/Hebrew],[Cross-ref],[Cultural],[Illustration]). Your voice stays central. Saved as a note you can edit in the Tiptap editor.Backend-driven session mode
— The companion tracks what mode you're in (prep, interview, pray) server-side. No more frontend guessing.
Structural tool gating
— The AI can't skip the interview. The
save_preptool is withheld until you've had enough exchanges. When you say "done," the AI is forced to call the tool instead of just saying "Building now."Reading level matching
— Prep output matches your vocabulary and tone. If you write simply, the note reads simply.
Full-page build animation
— When the AI builds your prep note, a loading overlay shows progressive status messages instead of a generic spinner.
Socratic interview mode
— Deep-dive conversations with pull-quote transcript UI.
Capture and note attachments
— Attach your stream captures and notes to companion messages as context.
Session persistence
— Leave and come back. Your conversation picks up where you left off.
`/pray`, `/interview`, `/help` commands
— Quick-access slash commands for common actions.
Stream
- Scripture auto-detection with expandable pills.
- Chronological feed with timestamps.
- Promote stream entries to notes (new or append to existing).
Notes
- Tiptap rich text editor with slash commands (`/` menu: headings, lists, quote, divider).
- Selection toolbar (floating: bold, italic, highlight, headings, list, quote).
- Soft-gold text highlighting.
- Properties drawer with stage stepper (Capture → Study → Finished) and type tags.
- Auto-save with 800ms debounce.
- Filter tabs: All, Capture, Studying, Finished, Threads, Teaching, Prep.
- Scripture block extension (insertable via `/` menu).
Prayer Board
- Kanban columns: Active, Waiting, Answered, Ongoing.
- Drag-and-drop between columns.
Knowledge Graph
- Auto-extraction pipeline: concepts, edges, and relationships from your notes and captures.
- Knowledge dashboard (`/knowledge`) with type filters and manual sync.
- Concept detail pages with delete and merge UI.
- Auto-generated concept summaries.
Search
- Postgres full-text search with GIN indexes.
- Global search bar in sidebar.
Import
- Import from Markdown, TXT, CSV, DOCX, and PDF files.
Proper markdown-to-Tiptap conversion (headings, lists, bold, etc.
).
Auth
- Neon Auth (managed Better Auth) with middleware route protection.
- Sign in/sign up page.