Devlog · Lumenor

Lumenor: A Socratic AI Tutor, and a Rejection From Apple

PJ By PJ Geldenhuis · July 15, 2026 · 12 min read · In App Review · getlumenor.com

How this was put together: reconstructed from session transcripts and file timestamps in the project's repo, not a diary kept at the time. Dates are inferred from slice docs and script timestamps, which lined up closely with what the sessions described. One unrelated session about a completely different industrial-IoT project was excluded entirely, as was a rejection-fix effort still in progress at research time.

Key milestones

Lumenor — Learn anything, properly
Lumenor's "North Star" mark — a four-point star with an elongated top ray — from the project's official brand identity.
getlumenor.com
Lumenor live homepage screenshot
The live marketing homepage today, July 15 — the app itself is behind a login and still in App Review.

The tool stack, end to end

Framework / HostNext.jsVercelTurborepoCapacitor (iOS)
AuthClerk + custom native token bridge
DatabaseSupabase
Tutor AIClaude — Socratic tutor core
VoiceDeepgram (speech-to-text)ElevenLabs (text-to-speech)
Video avatarAnam.aiCartesia voices
Mobile CIXcode CloudTestFlight
Quality gate61-scenario red-team + jailbreak eval harness

What it is: a Socratic AI tutor for iOS and web — "the tutor that knows you." It compiles a personal curriculum for any goal you type, teaches by questioning rather than answering, tracks mastery on a visual skill map called a "Constellation," and offers voice and video-avatar tutoring alongside text.

Origin: before the repo existed

The project's own idea/ folder holds the paper trail: a PRD and market-analysis pass done under two different working names — "Lumi" and "Lumina" — plus a standalone research report ranking self-directed learning interests. Somewhere between that research and the first commit, the product was renamed Lumenor (a trademark sweep confirmed no conflicting education marks under that name). The "teaches by questioning, not answering" thesis and the constellation metaphor both survived from the original research into the shipped product essentially intact.

Day 1: the compiler, and a very rough infrastructure day

Goal: stand up the repo and prove the core loop — type any learning goal, get back a personalized skill map in under a minute.

What actually happened was mostly a fight with deployment infrastructure, not the product logic, in order: a branch-name mismatch between the local repo and the host; a database URL still holding literal placeholder text; auth keys never added to the deploy target; a missing lockfile that made the installer guess an ancient package-manager version; a strict environment-variable mode that silently stripped every variable not explicitly declared; a project misconfigured as a generic type instead of the actual framework; production auth keys that are domain-locked and silently refuse to initialize on a preview domain; background jobs that never registered with the deployed app at all; and a compile-race condition that let the same task run itself multiple times over, producing a visibly duplicated mess on the very first real test.

Every one of these was diagnosed and fixed the same day, each time from a pasted build log. By evening, five wildly different test goals — LLM internals, Dostoevsky, GCSE trigonometry, options trading, spoken Afrikaans — each compiled into a distinct, personalized skill graph in 19–25 seconds. The domain was purchased that same evening once the live demo worked. This was clearly the single hardest day of the project — not because the product logic was hard, but because nearly every piece of hosting/auth/build infrastructure had its own gotcha, each only discoverable by shipping and reading the real error.

Day 2: a full app skeleton, then a hard fight with native sign-in

In a single day, Lumenor went from a bare compiler to a mostly-complete app skeleton: the Socratic tutor core, a mastery/spaced-repetition engine, a voice scaffold, file uploads with retrieval, weekly insights, a paywall scaffold, and a native iOS shell.

Getting Sign in with Apple and Google working in that native shell turned into a genuinely difficult, multi-hour arc, because the root cause was two layers deep. The first hypothesis — a missing native-app registration in the auth dashboard — turned out not to be it; fixing it produced identical errors. The real cause, found only by reading the auth provider's own SDK source: the web SDK running inside the native shell has no native OAuth-token strategy at all — those only exist in dedicated native mobile SDKs. No dashboard setting could ever have fixed it; the fix had to be code. Even after that, Apple sign-in kept failing because its entitlement had been enabled mid-session, after the current test build had already been signed — requiring two more builds before it actually worked. Google worked immediately since it was pure web code needing no rebuild.

The marathon day: explanation lenses, and the eval harness doing its job

This was, by file timestamps, the single most productive day of the build — ten-plus feature slices and a full security pass in one sitting. The best "honest" story of the whole project is the explanation-lens feature, which lets a learner shift explanation register by tapping a control or just saying "explain it like I'm five."

Eval runResultWhat it caught
First run4 of 8 scenarios (50%)Asking for a simpler explanation caused the tutor to quietly hand over the answer disguised as an analogy
After prompt tighteningSame failure recurred a third timeProse alone wasn't reliably beating the model's "just be helpful" reflex
After a hard-coded example pair, run twice8 of 8 (100%)Only trusted once it passed twice in a row, to rule out judge-scoring noise

Notably, all three built-in jailbreak and answer-extraction probes passed on the very first run with zero leaks — the failures that took three rounds to fix were teaching-quality bugs, not safety bugs. The same day's 12-scenario red-team pack caught a real prompt-injection hole (uploaded document text flowing straight into the tutor's prompt, completely unframed) and surfaced that in-app account deletion — an outright App Store requirement — simply didn't exist anywhere yet. The full 61-scenario suite passed at 96.7% with zero answer leaks.

The paywall decision made this same day is worth calling out: faced with wiring a subscription paywall before submitting, or shipping free during the beta, the call was to ship free — specifically because a sibling app's only-ever App Store rejection had been a subscription-disclosure guideline. The reasoning: concentrate first-submission review risk on the surface that had just gotten real hardening, not on the least-tested feature. That decision turned out not to be quite risk-free either, as the next section shows.

A submission that wouldn't attach, solved by a sibling project's own history

Six consecutive build uploads showed up in the App Store's build-selection screen with their radio buttons permanently greyed out — no combination of clicking would let any of them attach to the version. The fix came from PJ pasting in troubleshooting notes from his other app, which had hit the exact same symptom before: builds produced with the CI workflow's distribution setting on "Internal Testing Only" are permanently ineligible for App Store submission, no matter what — that's the platform's design, not a bug. Changing one dropdown and cutting a fresh build fixed it. A good example of institutional memory paying off across two different apps in the same portfolio.

The rejection

Build 30 was submitted, and the review came back with three findings:

  1. The app icon read as a placeholder — it looked like an unfinished design-tool export.
  2. The app "appears to be a pre-release, test, or trial version." The trigger was the premium page's own "Beta" / "everything unlocked while in beta" copy — the very language chosen a week earlier specifically to reduce a different kind of rejection risk. It traded one risk for another.
  3. Sharing user data with third-party AI services needs an explicit in-app consent step naming exactly what's sent and to whom, before any of it happens — a privacy-policy page alone wasn't sufficient under this reading of the guideline.

A fully specific fix plan was written for all three: strip "beta" language everywhere including store metadata; add a one-time AI-data consent screen mirroring the existing age-gate pattern; replace the icon with a finalized brand mark. That work was still in progress at the time of this writeup.

Meanwhile: the video-avatar tutor

While the build sat in review, a video-avatar tutor feature shipped — two personas chosen on data rather than taste (matched lighting and expression so they'd read as a designed pair, voices picked using a numeric voice-trait API rather than by ear). Real dogfooding surfaced a hard cutoff after 3–4 minutes and a clunky voice mode. The cutoff's root cause was found by reading the provider's own session-analytics dashboard: the actual session had run for exactly the code's own default length cap, because an intended override wasn't reaching the deployed build. Fixed two ways — raised the default, and made the avatar transparently reconnect on any cap or network drop rather than ending the conversation at all.

A separate bug — overlapping text on both the avatar and voice screens, reported with six screenshots — turned out to be both canvases rendering the conversation twice: a floating decorative text line, the real transcript, and the controls all stacked on the same pixels. Fixed with a shared text-rendering component and both canvases rebuilt into one clean vertical stack, shipped the same session it was diagnosed in.

Where things stand now: Lumenor v1 was rejected on three grounds — a placeholder-looking icon, "beta" language that read as unfinished, and a missing AI-data consent step. A specific fix for all three was written and was being executed as a dedicated follow-up effort at the time of this writeup. Real V2 groundwork already exists ahead of getting v1 approved: the video-avatar tutor is live, and a five-slice roadmap is written down. The core loop — compile a goal into a skill map, teach it Socratically, track real mastery, hold the line against jailbreaks — has a 61-scenario eval suite passing at 96.7% with zero answer leaks as its release gate.

Open issues

  • App icon read as an unfinished placeholder to Apple's reviewer — needs the finalized brand mark shipped
  • "Beta" language across the app and store metadata read as an unfinished product — needs stripping everywhere
  • No in-app consent step yet for sharing data with third-party AI providers (tutoring, speech, voice)
  • Turn-pipeline latency measured at 3.28s p50 time-to-first-token — the real gap versus feeling "ChatGPT-smooth"

Upgrade opportunities

  • Ship the three-part rejection fix and resubmit for App Store review
  • Cut voice/avatar latency as Slice 19's main focus
  • Work through the five-slice V2 roadmap: Nightly Observation, Share-your-sky, certificates, camera-to-constellation, cross-track misconception memory
Today (July 15): Lumenor's first-ever avatar and banner were built for its future social presence — using the project's real, official brand identity (found in its design-handoff folder) rather than a guess. An earlier attempt, built from a color sampled off the live marketing site, turned out to be the wrong mark entirely and was fully rebuilt once the actual source files were located: the "North Star" icon shown above, at its exact specified proportions and color.

← Back to the Devlog