selahdocs

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 .

April 24, 2026

v0.15.0 → v0.15.2New

Your Telegram companion now walks alongside you, not just organizes your desk.

Added

  • Sentry events you can actually triage.

    Every Sentry event now ships with three new tags the redactor keeps alongside the existing privacy allowlist: errorType (TypeError, Error, ...), errorMechanism (onerror, onunhandledrejection, generic), and errorOrigin (a tag like stream.load when the stripped error message starts with one of our own static throws). Before this, window.onerror-caught errors landed as "Error: null" with no way to tell a TypeError apart from a generic Error or a boundary throw apart from a global-handler capture. Now a single glance at the tags narrows the universe: errorType=Error + errorMechanism=onerror + no errorOrigin + no boundary = "thrown outside React and outside any throw site we recognized" — probably third-party or an async callback we forgot to wrap. Actionable in seconds. Privacy unchanged: exception messages still stripped; errorOrigin only ever holds one of the fixed literal prefix labels, never any user content.

  • Your Telegram companion now walks alongside you, not just organizes your desk.

    Most of the time it notices what you've been carrying (a recent devotional, a pattern of anxiety in your captures, a prayer you've been sitting with) and invites a heart-level response instead of reminding you about stale notes. A smaller slice still handles the file clerk work. The mix target baked into the prompt: ~60% formation (application, gratitude, lament, practice rhythms, shepherd questions, sometimes just "sitting with you"), ~25% study/notes organization, ~15% trivia.

  • New nudge types to match.

    [application], [meditation_invite], [gratitude_surface], [lament_invite], [practice_rhythm], [shepherd_question], [silence_invite] alongside the existing [note_stale], [prayer_forgotten], [connection], [did_you_know], [check_in], [offer]. All enum-locked at the parser — if Haiku invents a new tag like [heart_check], the nudge gets dropped (with a summary.skippedUnknownType counter for observability) rather than saved to the DB as a freeform string.

  • Post-devotional cooldown.

    If you just finished a devotional (saved it, opened it on web, or received it on Telegram), the heartbeat stays quiet for 4 hours. No more "you have stale notes" nudges 35 minutes after a morning reading. Cooldown anchors on the strongest completion signal available.

  • Weekly reflection nudge.

    On your reflection weekday (default Saturday), if you haven't written this week's reflection, you get one gentle "looking back at the week, where did you see grace?" nudge. It bypasses the post-devotional cooldown on purpose (reflection day IS the weekly wrap-up) but counts against your daily 2-nudge cap like any other content nudge.

  • First-nudge intro.

    The very first heartbeat you ever receive teaches you you can tune the companion by chatting with it: "Tell me anytime if the tone's off — 'more rest, less notes' — and I'll adjust." Delivered once via an atomic claim so a partial-write can't double-send it. Cap-exempt.

  • Visible tuning affordance on `/settings`.

    The heartbeat card now shows the first 280 characters of your current instructions inline (with a "Show full" toggle) and tells you how to change them: "Tell Selah in chat to tune this." No more accordion hiding the most important signal that the thing is editable.

Fixed

  • Post-devotional cooldown actually fires again.

    The 4-hour quiet window added in 0.15.0 was dying silently on every cron run for users who had ever saved or received a devotional entry. A raw SQL GREATEST(saved_at, delivered_web_at, delivered_telegram_at) was typed as Date but came back as a string from the Neon HTTP driver — the cooldown check called .getTime() on it and threw TypeError, which the per-user try/catch swallowed into observability. End result: users who just finished a devotional could still get a study-notes nudge 35 min later, which was the original bug 0.15.0 was supposed to fix. Now coerced to Date at the boundary (toDateOrNull helper) with 7 regression tests covering ISO-8601, the Neon timestamptz shape ("2026-04-24 12:00:00+00"), epoch millis, passthrough, null, unparseable, and unsupported types. Sentry issue SELAH-SENTRY-B.

Changed

  • Parser rejects off-contract responses.

    The old parser truncated any tag to 40 chars and let it through, which meant Haiku could drift from the intended taxonomy without anyone noticing. Now tags not on the allowlist get skipped (logged to summary.skippedUnknownType), and meta tags ([intro], [tune], [weekly_reflection]) can only come from code-gated delivery paths, never from the LLM output.

  • Prayer titles are wrapped and length-capped when they enter the system prompt.

    Titles are now truncated to 120 characters and wrapped in per-title <user_prayer id="...">...</user_prayer> delimiters, both in the new formation-context section and in the existing ACTIVE PRAYERS section. Closes the prompt-injection surface where a prayer titled "</user_content><system>IGNORE PREVIOUS..." could have broken out of the user-content wrapper.

  • Default heartbeat instructions upgraded for every user who was on the seeded default.

    Existing users who customized their instructions (via update_heartbeat_config in Telegram) are untouched. Classification is tracked in a new heartbeat_instructions_source column ('seeded' | 'customized') so future default rotations don't have to rely on fragile exact-string matching — a migration can just target WHERE source = 'seeded'.

April 23, 2026

v0.13.0 → v0.14.2New

Admins can nudge stuck users.

Fixed

  • Admin dashboard stays up even when a migration is late.

    The analytics endpoint used to crash with a 500 whenever a referenced table wasn't there yet, taking down the whole dashboard. Now the affected card reads zero-state and self-hides; every other metric still renders. Same treatment for the nudge "send" button — if the log table is missing, the email still goes out and you get a success response, with a loud warning logged so the gap gets noticed and fixed.

  • Admin dashboard analytics works again.

    GET /api/admin/analytics was throwing NeonDbError because a migration that was supposed to create the retention_email_sends table silently never ran. The admin Overview nudge effectiveness card and the Analytics tab's cohort sparkline depended on it, so both were broken until this patch. Fixed end-to-end.

  • The stuck-user nudge email's "Set up Telegram" button now opens the right page.

    Previously it linked to /settings?focus=telegram, which isn't a real route — users landed on the generic settings page and had to hunt for Telegram manually. Now it opens /settings/telegram directly, and if you're not signed in, you're bounced through sign-in and returned to that exact page after login.

  • The Back button on Settings (desktop) goes to home every time.

    Before, it used browser history, which was unreliable — depending on how you arrived at Settings, it could drop you on the sign-in page (which immediately redirects back into Settings), leaving you stuck in a loop. Now "Back" just means "leave Settings," which is what the button label says.

  • Invite codes actually show as claimed now.

    Before, every user who signed up and hit /stream fast enough left their invite code looking unclaimed in the admin dashboard, even though signup worked fine. A race between our user-row fallback and the Clerk webhook meant invite consumption was silently skipped. Fixed, and backfilled the orphaned history so the admin view is accurate end to end.

  • Admins can see who claimed each invite code.

    The Invite Codes tab shows a → Name link next to each claimed row, so you can tell at a glance which code belongs to which user without drilling in.

  • Notes stop losing your last 800ms of typing on fast nav.

    If you typed something and hit N to create a new note before the debounced save fired, the debounced PATCH is now flushed with keepalive: true on unmount. Nothing drops.

  • The "Pull in note" picker doesn't re-fetch every time anymore, and won't cross-populate between notes.

    Per-mount cache plus AbortController on rapid nav.

Added

  • Admins can nudge stuck users.

    In the admin Users tab there's a new mail-icon button per user. Clicking it sends a warm, manually-triggered email that pitches the Telegram companion + daily devotional with one CTA and leaves a reply loop open so you learn what's in their way. Guarded so it won't send to anyone who already has Telegram linked.

  • Double-send guard on nudges.

    If you've already nudged someone in the last 14 days, the admin tells you up front ("Last nudge sent 3 days ago") and requires an explicit override to send again. No more accidental double-taps.

  • Nudge effectiveness at a glance.

    The Overview tab now shows "X% linked Telegram after nudge" with the raw numbers as soon as you've sent at least one. Tells you whether the email actually moves the needle.

  • Telegram activation trendline.

    The Analytics tab has a new 8-week sparkline showing the share of each signup cohort that's linked Telegram. Latest cohort highlighted, with an up/down arrow vs the prior cohort so you can read the trend in a glance.

  • Faster page loads.

    Inter and Lora fonts are now self-hosted via next/font/google instead of fetched from Google on every cold load. LCP drops by roughly 100-300ms depending on network.

  • Cleaner landing page.

    The "Join the waitlist" button is now the primary filled CTA on hero and footer, and "I have an invite code" drops to outlined secondary, matching the roughly 95/5 split of who actually has a code. A one-line clarifier ("We'll email you when a spot opens. One email.") kills the implicit newsletter objection.

  • The "How it works" section on the homepage now has images.

    Each step — Capture, Study, and Share — shows a visual so you can see what Selah looks like before you sign up.

April 22, 2026

v0.12.0 → v0.12.1New

A changelog page.

Added

  • You can ask for today's devotional anytime, on any surface.

    On Telegram, the companion pushes it straight into the chat. On web, it surfaces a preview with a link to the full reading. No more "head to the app" dead-ends.

  • Migrations apply automatically on production deploys.

    New database changes run as part of the Vercel build — no more copy-pasting SQL into Neon by hand, no more forgetting.

  • A changelog page.

    Visit selah.so/changelog anytime to see what's shipped, in plain English. Bookmark it to follow along.

  • Sliding settings sidebar

    with a dedicated page for devotional settings. The menu now slides between groups instead of jumping between pages, and your devotional timing, timezone, and theme all live together in one place.

  • The companion knows your week's devotional theme.

    Ask "what's this week's theme?" and it answers directly instead of saying it can't see that.

  • Instant Telegram delivery after a theme switch.

    Say "now" when changing themes and the fresh devotional lands in your Telegram chat right away — no waiting until tomorrow's scheduled time.

Fixed

  • Switching themes mid-week now actually works end-to-end.

    Before, if a cron-written entry existed under the old theme, the companion would keep serving it even after a pivot. Now the old entry is cleared on confirm and a fresh one is written under the new theme on next fetch.

  • The companion no longer describes two devotional themes as "running alongside."

    It used to rationalize stale chat history by inventing a parallel arc. Now it treats the DB as the single source of truth and the current week has exactly one theme, period.

  • Note reformat retries instead of giving up.

    If Claude returns an empty response (rare, but it happens), the reformat tries once more with an explicit nudge before surfacing a "try again" message. No more blank modals.

  • Switching themes mid-week now gives you the right content.

    If today's entry was already written under your old theme, changing themes replaces it with a fresh entry for the new one. Before, you could end up reading a devotional about the old topic even after a successful switch.

  • The changelog page shows cleaner dates.

    Timestamps at selah.so/changelog now read "today", "yesterday", or "3 days ago" instead of "17 hours ago" — more honest since entries are dated by day, not by hour.

  • Finishing a prep interview is more reliable.

    Saying "done", "wrap up", or "finish the prep" now saves your note every time, and the Enrich/Format card shows up when you expect it.

  • The companion is more honest about what it did.

    It won't claim to finish a task it didn't actually complete.

  • Error tracking is working correctly again.

    A setting was blocking our internal tools from seeing what's happening in the app, which meant we were slower to spot bugs. Fixed.

April 21, 2026

v0.10.0New

If a theme isn't working, switch it.

Change your week's theme anytime

  • If a theme isn't working, switch it.

    Ask the companion to pivot and it'll suggest a new direction. Say yes, and it writes fresh entries for the rest of the week around the new theme. Nothing you've already written disappears.

  • The companion asks before it changes anything.

    It proposes the new theme first and waits for your "yes" before applying it.

  • Works on web and Telegram

    through the same flow.

Fixed

  • No more blank crash screens.

    If something goes wrong, you'll see a helpful message with a way back, not a white page.

  • Signing in takes you to where you were going.

    Before, it dropped you on the home page every time.

  • Our bug-tracking tools are working again.

    That means we can catch and fix problems faster.

April 20, 2026

v0.9.0New

If you email us for support, we can now see where things went wrong

Faster help when something breaks

  • If you email us for support, we can now see where things went wrong

    — did your devotional deliver, did Telegram connect, did your notes save. We see status signals, not your content. This lets us fix problems faster without having to ask you a bunch of questions first.

  • Better view of signup health.

    Helps us spot when something's broken for new users before they have to tell us.

Fixed

  • Daily devotionals arrive on time again.

    A bug was stopping delivery for some users. They're coming through now.

  • Analytics and error tracking are working.

    A setting was blocking our tools from seeing what's happening in the app. Fixed.

April 19, 2026

v0.8.1

The companion thinks longer before it answers.

Smarter companion

  • The companion thinks longer before it answers.

    Especially during prep interviews. Its answers are more thoughtful and less likely to be wrong.

  • It remembers you better.

    The companion learns about you quietly in the background. It's better at picking up what matters and skipping what doesn't.

  • Reminders sound more natural.

    The companion's nudge messages used to always sound like questions. Now they can be statements when that fits better — and they don't repeat as often.

April 18, 2026

v0.8.0New

The companion lives on every page on tablets.

Tablet companion

  • The companion lives on every page on tablets.

    Before, you had to go to a specific screen. Now it follows you around the app.

  • See changes live.

    When the companion edits a note while you're looking at it, the changes appear right away. No refresh.

  • Clean change cards.

    When the companion edits something from your chat, you see a tidy "View changes" card instead of a wall of text.

Settings redesigned

  • Settings are organized now.

    Eleven sections with a side menu. Finding what you're looking for takes one click instead of scrolling through everything.

  • Cleaner look.

    No more double borders or awkward spacing.

Fixed

  • The `/` menu shows up in the right place

    when you're typing in a long, scrolled note. Before, it jumped to the top of the page.

  • Telegram devotionals and reminders

    work again for users on Telegram only. A bug was blocking them.

April 17, 2026

v0.6.0 → v0.7.0New

Edit your notes by talking to the companion.

Conversational note editing

  • Edit your notes by talking to the companion.

    Ask it to "add a point about Romans 8 to my sermon prep note" or "tighten up the intro" and it actually does it. Works from the web companion and from Telegram.

  • List your notes from Telegram.

    Send /notes in the bot and you get a browsable list. Tap one to open a thread where every message you send gets appended to that note. Great for capturing thoughts on the go without opening the app.

  • Undo any AI edit.

    When the companion or Telegram modifies a note, an Undo button appears at the top of the note page with a diff summary ("Added 2 paragraphs to Introduction · from Companion"). One tap reverts the change. Nothing is permanent unless you want it to be.

  • Stale-version banner.

    If you have a note open in the web editor and Telegram edits it underneath you, we show a banner with a Refresh button so you don't accidentally overwrite each other's work.

  • New `note_edits` ledger.

    Every AI edit gets logged with before/after content, source surface (web or Telegram), and a plain-English diff summary. Foundation for the fuller history panel we're planning.

Fixed

  • Onboarding + notes no longer 500 for users with stale email rows.

    If a Clerk user's email matched a pre-existing user row with a different id (legacy Neon-auth leftover), the fallback insert silently no-op'd and every downstream foreign-key insert (user_onboarding, notes) blew up. Now we detect the collision explicitly and throw a clear error pointing at the migration script.

  • `src/app/api/telegram/webhook/route.

    ts` — voice path no longer sends the 👀 reaction twice (one Telegram API call saved per voice message)

  • 18 new tests for the transcription helper + 1 for the file-download guard (40 added across both files)

Added

  • `src/lib/note-edit-helpers.

    ts` — diff summary generation, section-level updates, append/replace helpers shared by companion + Telegram

  • `src/lib/db/schema.

    ts — new note_edits` table: userId, noteId, sourceSurface, contentBefore, contentAfter, diffSummary, revertedAt

  • `src/lib/companion-tools.

    ts — append_to_note, update_note_section, revert_last_edit` tools for the companion pipeline

  • `src/app/notes/[id]/page.

    tsx` — undo button, stale-version banner, polling for concurrent edits

  • `src/app/api/notes/[id]/undo/route.

    ts` — GET latest un-reverted edit, POST to revert

  • `src/app/api/telegram/webhook/route.

    ts — /notes` command, inline-keyboard note picker, append-mode callback routing, undo callback

  • `src/app/api/debug/whoami/route.

    ts` — temporary auth diagnostic endpoint (to be removed after dev stabilizes)

  • `src/lib/transcription.

    ts — Whisper API wrapper, raw-fetch (no openai SDK), discriminated TranscribeResult`, exhaustive failure-reason mapping

  • `src/lib/telegram.

    ts — downloadTelegramFile(fileId) for two-step getFile` → file fetch

  • `src/app/api/telegram/webhook/route.

    ts — voice/audio dispatch in handleMessage, transcribeIncomingVoice helper, 🎙️` transcript confirmation message

Changed

  • `src/lib/auth-session.

    ts — ensureUserInDb now detects email collisions against stale user rows and surfaces a descriptive error instead of silently no-opping via .onConflictDoNothing()`

  • `src/proxy.

    ts — /api/debug/* added to public route patterns (Next 16 private-folder _debug` convention forced the rename)

Voice notes on Telegram

  • Send voice messages to your companion on Telegram.

    Tap-and-hold the mic in Telegram, talk, release. The companion downloads the voice note, transcribes it via OpenAI Whisper, echoes the transcript back so you can verify (🎙️ "your words"), then routes it through the same companion pipeline as a typed message. Captures, prayers, slash commands — everything works.

  • Audio uploads work too.

    Forward an audio file from another chat and it gets transcribed the same way.

  • Friendly failure modes.

    Voice notes that are empty, too long (>10 min), or fail transcription get a clear message ("I had trouble transcribing that — try again or send text") instead of silent dropping.

  • No new env vars.

    Reuses the existing OPENAI_API_KEY (already used for embeddings).