Active · May 23, 2026
Mimi
Turn any YouTube video into spaced-repetition English shadowing, plus daily drills for the grammar patterns, collocations, and prepositions that never come automatically. Java/Spring + Next.js, built solo.
8,892 LOC · 49 endpoints · 121 tests · multi-tenant · AWS ECS Fargate
- Role
- Solo (AI-paired with Claude Code)
- Stack
- Java 21 · Spring Boot 3.3 · PostgreSQL 16 · Next.js 16 · TypeScript · Gemini / Claude · AWS ECS

A full-stack English-learning app for developers who read English fluently but freeze when they have to produce it. Java 21 / Spring Boot API + Next.js / TypeScript frontend, now shipping on the iOS App Store as Mimi — English Shadowing. The thesis is friction removal, not feature count: turn the input you already consume (YouTube) into the output practice you avoid.
Two halves that feed each other
- Shadow from YouTube. Paste a URL, clip a subtitle range, and one LLM call returns a translation, a word-by-word literal gloss, vocabulary, per-preposition notes, and a practice scenario — cached in Postgres JSONB so studying never re-bills the API. SM-2 spaced repetition schedules each clip; review in reveal / write (token-level diff) / scenario modes.
- Practice hub. Daily Leitner-spaced drills over 82 sentence patterns, 101 word + preposition collocations, animated preposition diagrams, an AI composition check, and a weak-spots dashboard built from your own lapse data.
Engineering highlights
- Transaction-safe async AI pipeline. Creating a clip publishes a domain event; analysis runs on a background thread via
@TransactionalEventListener(AFTER_COMMIT)+@Async, so the multi-second LLM call runs outside any DB transaction and never pins a connection-pool connection. Only the smallPENDING → READY / FAILEDwrites are transactional; transient provider failures (429 / 5xx / timeout) retry, permanent ones fail fast. - Env-gated edges. The same code runs locally or on real infra by environment variable alone: the AI provider (Gemini ↔ Claude) flips via
@ConditionalOnPropertywith no call-site changes; recording storage (local disk ↔ S3 / R2) flips the same way, bytes always streamed through the backend, never a public URL. - Two SRS algorithms, matched to the grading shape. Clip review uses SM-2 (four grades); the binary drills (Got it / Again) use a Leitner box instead — forcing SM-2's 0–5 scale onto two buttons is the wrong tool. Both schedulers are I/O-free pure functions.
- $0 marginal cost + abuse guards. One LLM call per clip, cached; every later review reads the cache. The one endpoint that costs money — the AI composition check — is rate-limited per user, separate from the per-IP limiter on signup / login.
- Multi-tenant safety. Stateless JWT with a token-version claim (a password change revokes older tokens), per-user query isolation (
findByIdAndUserId— another user's id returns 404, not their row), BCrypt, and access-gated recordings.
Stack & infra
Java 21 · Spring Boot 3.3 · PostgreSQL 16 (Flyway, JSONB, raw-SQL CHECK) · Next.js 16 / React 19 / TypeScript strict · Tailwind 4 · shadcn/ui. Observability: Micrometer + Actuator → Prometheus, MDC request / user logging. Docker (multi-stage, ffmpeg + yt-dlp); GitHub Actions CI; keyless OIDC deploy to AWS ECS Fargate + RDS; frontend on Vercel. Tested with Testcontainers (real Postgres) + Vitest + Playwright (14 specs).
What it doesn't do
- Solo, AI-assisted — the product direction, architecture (provider abstraction, the transaction-safe async pipeline, the two-SRS split), prompt design, and the content-accuracy audit are mine; Claude Code was the pair.
- Rate-limit and idempotency are in-memory — correct for a single instance; multi-instance needs a durable store (Redis).
ja/zh/eslocales are scaffolded (English strings); onlyenandkoare fully translated.- AWS infra is a documented runbook + task definition, not yet Terraform.
- Subtitle extraction uses
yt-dlp, a YouTube ToS gray area — fine for personal use, revisit before anything commercial.
Project log
Chronological record of troubleshooting, retros, and updates while building this.
Build log
Week of Jul 13, 202614 entries · 3 Update · 1 Troubleshoot · 6 Tech retro · 4 UX retro
Consolidating a month of scattered branches into main — and hitting the EAS free-tier wall again
TroubleshootJul 17, 2026 · 3 min
main had drifted 10 days stale while a month of mobile work lived on ~20 unmerged branches. Verified via the EAS API that TestFlight was still build 18 (= old main), trial-merged the integration tip clean, consolidated it into main, pushed — then the new TestFlight cut died on the EAS free-plan iOS build limit, exactly as it had before.
Google Play readiness now has explicit signing and policy gates
Tech retroJul 16, 2026 · 2 min
Prepared the Android AAB/internal/draft profiles, removed release-only permission drift, and documented the Play Console work that still requires owner evidence.
Visual review narrowed the iPhone screenshot candidate set
UX retroJul 16, 2026 · 1 min
All four captures meet Apple's pixel dimensions, but only Home and Write remain visual candidates after integration review.
iOS and Android store preparation now share one integrated candidate
Tech retroJul 16, 2026 · 2 min
Merged both store-readiness tracks, rebuilt the integrated mobile candidate, and kept production signing and policy gates explicitly open.
Mimi's tab bar now reflects its three equal learning pillars
UX retroJul 16, 2026 · 2 min
Promoted YouTube shadowing and writing into the primary navigation, kept speaking as the raised center action, and moved review and the full practice library behind clear Home entry points without deleting their routes.
Android release audit: native builds pass, authenticated flows still need evidence
Tech retroJul 15, 2026 · 4 min
The first Android native pass fixed platform symbol weights, safe-area handling, portable monospace fonts, and microphone-denial recovery. APK and production AAB artifacts were produced, but Expo dependency checks and authenticated product flows remain open.
Cards were truncating their labels; make them grow to fit instead
UX retroJul 15, 2026 · 1 min
Practice pack cards and Home tiles capped text at two lines, clipping labels mid-word — worsened once U4 added a mastery block to the pack cards. Removed the line caps so cards grow to fit and row siblings match height.
Close Expo SDK 56 dependency drift and verify the packaged API target
Tech retroJul 15, 2026 · 2 min
Seven Expo dependencies were aligned to SDK 56, Doctor reached 21/21, and fresh Android artifacts were rebuilt with an independently verified production API URL.
Keep Android symbol fonts out of the iOS bundle
Tech retroJul 15, 2026 · 2 min
Android symbol weights rendered correctly, but their static imports silently added three Material Symbols fonts to iOS. A Metro platform-file split preserves Android fonts while removing them from iOS exports.
Home now shows honest practice mastery without inventing weekly activity
UX retroJul 15, 2026 · 3 min
U2 adds a compact, theme-aware Home mastery bar backed by real SRS and daily PracticeProgress data, plus a once-per-day evening streak-risk line that disappears after practice.
Integrated the four UX-motivation tracks (U1–U4) and closed the first-sparring seam
Tech retroJul 15, 2026 · 2 min
Merged three parallel Codex tracks (home mastery, session goal-gradient/peak-end, pack progress/milestones) on top of U1's data layer, resolving only two expected conflicts, then wired the one cross-track seam U4 deliberately left open. A pre-merge ground-truth check caught a branch ref that still carried a supposedly-reverted commit.
F1 — A read-only sparring report that preserves client-owned SRS keys
UpdateJul 13, 2026 · 1 min
The backend can now analyze a client-posted learner transcript into used targets, misses, corrections, and recurring mistakes without guessing or mutating client-owned SRS state.
F2 — Threading a job description through the mock-interview backend
UpdateJul 13, 2026 · 1 min
Mock-interview requests can now carry an optional, capped job description while preserving the existing no-JD prompt and strict question response contract.
F6 — Enforcing the existing paid plan at every AI boundary
UpdateJul 13, 2026 · 1 min
Every AI-backed practice route now uses the existing user-plan entitlement, while retaining a narrow email override for owners and testers.
Week of Jun 29, 20265 entries · 4 Update · 1 UX retro
1,956 example sentences the verb pack never had — generated in parallel, merged at build
UpdateJul 2, 2026 · 2 min
The base-verb pack showed a Korean cue and an English pattern but no sentence using it. Generated one natural example + Korean translation for all 1,956 entries with a 103-agent workflow, and merged them as committed side-data so the hand-editable source stays clean.
The verb pack was there all along — three discoverability bugs, one furious founder
UX retroJul 2, 2026 · 2 min
The founder installed the new build and could not find the verb pack no matter what. It wasn't missing — it was mislabeled, buried behind a hidden tab, and served as a flat 1956-card firehose. Fixed all three and shipped an axis-browsable pack that still feeds the same SRS.
A pack for telling an AI to write code — added end-to-end on mobile and web
UpdateJun 30, 2026 · 2 min
Added a 108-entry study pack of the natural English a developer uses to instruct an AI coding assistant, authored by hand and wired through the existing pack pipeline on both clients — the only non-mechanical step was regenerating expo-router's typed routes.
From shadowing app to study platform: a cross-pack drill engine on mobile and web
UpdateJun 30, 2026 · 2 min
Added a 1,956-entry base-verb pack plus four study lists, gave every pack recall/reverse/compose modes, and built AI mix + story generators — shared across mobile (Expo) and web (Next.js), with the data/SRS/backend living in one core package.
Shipping Mimi 1.1.0 to TestFlight — and the EAS build-credit wall
UpdateJun 30, 2026 · 2 min
Cut Mimi 1.1.0 (the cross-pack study platform) to TestFlight through the existing eas build --auto-submit pipeline, verified the build VALID straight from the App Store Connect API, and hit the EAS included-build-credits wall on the way out.
Week of Jun 22, 20261 entry · 1 Troubleshoot
Week of Jun 15, 20268 entries · 1 Troubleshoot · 2 Tech retro · 4 UX retro · 1 Business
A recruiter-facing case-study page: promoting a shipped app from a web portfolio
BusinessJun 21, 2026 · 2 min
Built a public /case-study page for Mimi so the app can be promoted from a web portfolio — live status pills, a browser demo, the system architecture, and the engineering decisions, framed for a backend/working-holiday job search.
Making Mimi feel physical: skeletons, haptics, and a mic dead-end fix
UX retroJun 20, 2026 · 4 min
Replaced bare loading spinners with skeleton placeholders, added a thin expo-haptics layer to every drill outcome, and turned a silent denied-mic failure into a Settings deep-link. The honest catch: none of it is verifiable until the next native build.
Five doomed builds to a locked phone: a floundering post-mortem
Tech retroJun 17, 2026 · 3 min
I rebuilt to a locked physical iPhone five-plus times before switching to the simulator — and only because the user asked. An honest accounting of what was tunnel-vision and what was a real environment wall.
Subtracting your way to a 'real app': Mimi's 6→4 tab restructure
UX retroJun 16, 2026 · 4 min
A code-grounded audit said Mimi read like a feature toolkit, not a daily app. The fix was mostly deletion — 6 tabs to 4, a feature-grid home to one action, a mixing-desk video screen to a script — plus finally giving the output drill real AI feedback.
A first-run that doesn't dump you on an empty screen
UX retroJun 16, 2026 · 1 min
New signups were landing straight on an empty home with nothing to tap. Added a 3-step onboarding — gated on the signup path so it never bounces existing users.
Progress you can feel, a resume that's actually where you left off
UX retroJun 16, 2026 · 2 min
Three retention refinements: a 'what I've built' stats card (no new backend), a Continue button that resumes the clip you actually last opened, and a Practice tab that stops dumping everything on first view.
Moving the backend AWS (Toronto) → Naver Cloud (Seoul): one box, ~10x cheaper
Tech retroJun 15, 2026 · 3 min
The Mimi backend was on AWS ECS in ca-central-1 (Toronto) — wrong on cost (~$50–65/mo) and latency (~180ms) for a Korea-targeted service. Migrated it to a single Naver Cloud (Seoul) box running everything in docker-compose behind Caddy, cut over DNS, and tore AWS down. Cost dropped to roughly a credit-covered ~₩50k/mo.
Five things that bit during the Naver Cloud (Seoul) migration
TroubleshootJun 15, 2026 · 3 min
The AWS→NCP move worked, but five NCP/Caddy specifics each cost a debugging loop: KVM block-storage args, a 10GB boot disk, NCP's root-password SSH model, ACME failing on a stale DNS then a CAA lock, and a pot-provider port mismatch. Symptoms + fixes.
Week of Jun 8, 202610 entries · 6 Update · 1 Tech retro · 3 UX retro
Mimi mobile gets a face — Home redesign, a named color system, and native tab icons
UX retroJun 13, 2026 · 2 min
The Mimi mobile app moved from placeholder Expo art and an emoji tab bar to a branded Home screen, a role-named light/dark color system, and SF Symbols tab icons — a visual rebrand with no behavior change, committed mobile-only.
Mobile shadowing: split a clip into sentence A–B loops
UpdateJun 13, 2026 · 2 min
Shadowing wants to drill one short line on repeat, but the player could only loop the whole clip. Reused the video's already-stored sentence timestamps to split a clip into tappable sentence chips, each an A–B loop. Zero new backend.
Mobile shadowing: three active drills + STT pronunciation feedback
UpdateJun 13, 2026 · 2 min
The mobile player mined rich per-clip data (chunked translation, practice scenario, transcript) and rendered it as read-only boxes. Turned it into active drills on the three pillars of fluency — listen, reorder, produce — plus opt-in STT pronunciation feedback. Zero new backend/AI/migration.
Mobile clip player: pin the video, tab the drills
UX retroJun 13, 2026 · 2 min
The clip player had grown into a wall of ~8 stacked cards — to do a drill you scrolled the video off-screen, in a shadowing app where the video is the point. Repinned the video as a fixed header and put the drills behind Listen/Order/Speak/Shadow/Notes tabs.
Every Korean string needs a declared role: rebuilding 502 cues in English word order
UpdateJun 10, 2026 · 2 min
The drill app's production cues had drifted into natural Korean word order — the exact thing the method forbids. A dedicated cue field in English word order (직독직해) was generated and order-verified for all 502 items, and an argumentation-moves layer (67 moves in 8 functions) was added for actually defending one's thinking.
Phrasal verbs are a system, not a list: the particle workshop
UpdateJun 10, 2026 · 2 min
Instead of drilling 'kick off' and 'roll back' as isolated flashcards, the new workshop groups phrasal verbs by particle and teaches each particle's core spatial image first — so unseen phrasal verbs become guessable. Plus 72 real dev collocations, and the Practice tab finally shows all its tools.
Authoring a 150-item dev-English drill bank with a verify-after-author workflow
UpdateJun 9, 2026 · 2 min
The interview-prep app needed a big bank of real spoken dev English — phrasal verbs, expressions, code-narration, connectors. A workflow authored 4 banks in parallel and a second adversarial agent cut 205 drafts down to 150, enforcing 'what an engineer actually says, not textbook'.
From flashcards to a live AI interviewer — four training modes in one pass
UpdateJun 9, 2026 · 2 min
The drill app got its missing layer: an AI mock interviewer that digs into your spoken answers, a connector-cloze chaining drill, a speed round, weak-card repair, and a 60-second speech mode — plus a fix for category drills that silently hid most of their cards behind SRS pacing.
Brevity budgets are per-layer: enriching 309 terse flashcards with real explanations
UX retroJun 9, 2026 · 2 min
The drill cards were optimized for brevity everywhere — including the reveal, where learning actually happens. A fan-out enrichment pass added a friendly Korean nuance/when-to-use/pitfall explanation and an example translation to every one of the 309 phrase cards, with a one-shard failure recovered standalone.
Migrating prod to Toronto — and recovering a terraform state I killed mid-write
Tech retroJun 8, 2026 · 3 min
Moving the backend from Seoul to ca-central-1 to cut mic latency turned into an incident: I SIGKILLed a stuck terraform apply, truncated the state file to zero bytes, and had to re-import 49 orphaned AWS resources back into state instead of deleting and rebuilding.
Week of Jun 1, 202627 entries · 10 Update · 7 Troubleshoot · 10 Tech retro
On-device speech recognition can't do dev jargon — moving the interview mic to Whisper
Tech retroJun 7, 2026 · 3 min
The interview-prep mic went through a crash, an AirPods audio-session interruption, and a dictionary-biasing dead-end before the real fix landed: Apple's on-device recognizer simply cannot transcribe terms like 'idempotency', so we record and send to Whisper large-v3 (Groq free tier) instead.
A provider fallback chain so a free Gemini key can spill over to OpenAI
Tech retroJun 4, 2026 · 2 min
The app called a single AI provider chosen by config. Turned it into a priority chain — Gemini → OpenAI → Claude — that falls back on quota/error, so free-tier Gemini serves everyday traffic and spills over only when it's full.
First AWS deploy, three walls: a CAA record, an un-issued cert, and a free-tier backup
TroubleshootJun 4, 2026 · 2 min
The first terraform apply of the backend to ap-northeast-2 mostly came up — then RDS rejected the backup window, the HTTPS listener refused a cert that wasn't issued yet, and ACM validation FAILED on a CAA record Vercel had quietly set. Three classic first-deploy traps, in order.
The whole backend deploy, as Terraform you can read line by line
Tech retroJun 4, 2026 · 4 min
Turned the manual AWS console runbook into complete, validated Infrastructure as Code: VPC, RDS, S3, Secrets, ECR, IAM with keyless GitHub OIDC, ALB+ACM, and ECS Fargate — one file per concern, commented to teach. The design decisions (no NAT, who-owns-deploys, secrets generated not typed) are the part worth keeping.
`expo lint` isn't read-only — it rewrites package.json on first run
TroubleshootJun 4, 2026 · 1 min
Running `npx expo lint` once to check a new screen quietly added eslint + eslint-config-expo to package.json, regenerated the lockfile, and scaffolded an eslint.config.js — none of it part of the change being made. A reminder that 'lint' commands can mutate the repo.
The CI job that failed every push after the monorepo move
TroubleshootJun 4, 2026 · 2 min
A failure email on every push to main, all from one GitHub Actions job dying at 10s: 'Some specified paths were not resolved, unable to cache dependencies.' The frontend CI job still assumed the frontend was the repo root — its lockfile cache path and npm ci both pointed at the wrong place after the monorepo migration.
Localizing the mobile app to Korean — one agent per screen, and the entity that doesn't decode outside JSX
Tech retroJun 4, 2026 · 2 min
The Expo app was English-only despite a Korean audience. Added a lightweight i18n layer and translated all 18 screens by fanning out one agent per file — each edits its screen and returns its dictionary slice — then merged the slices. The bug worth remembering: an HTML entity that JSX silently decodes becomes a literal once the string lives in a data layer.
Don't co-install a React Native app and a web app in one npm workspace
TroubleshootJun 4, 2026 · 1 min
Turning the repo into a monorepo broke the Vercel web build — installing the workspace pulled in the Expo/Metro dependency tree, including a native module with no Vercel/linux prebuild. The fix: keep the cross-platform app out of the workspace and share code through a file: link.
Sentence gym v2: one sentence, ~60 graded sub-variations
UpdateJun 4, 2026 · 2 min
v1 bent a base sentence through 15 grammatical categories. v2 splits each category into its essential sub-variations — ~52 core + ~15 extra — and adds a 0-100 AI score on the optional self-check, turning one sentence into a ~60-rep mastery drill.
Daily sentence gym: bending one mined sentence 15 grammatical ways
Tech retroJun 4, 2026 · 3 min
A new daily drill that takes one base English sentence and rewrites it through 15 grammatical operations. The interesting part wasn't the feature — it was the shared LLM client's hardcoded 600-token cap that would have truncated the output JSON on the default provider.
The Vercel deploy that failed four native binaries deep
TroubleshootJun 4, 2026 · 3 min
Merging the monorepo to main and pushing to Vercel surfaced a chain of missing linux native prebuilts — @parcel/watcher, then @next/swc, then Tailwind's oxide and lightningcss. The lesson: when lockfile pins turn into whack-a-mole, change the install itself.
Web and app are two surfaces, not one — porting the gym to the Next.js frontend
Tech retroJun 4, 2026 · 2 min
The sentence gym was built on the Expo mobile app first. Trying to 'see it on web' surfaced a basic truth that's easy to forget in a monorepo: the Next.js frontend and the Expo app are separate surfaces that share only the core package — a feature in one isn't in the other until it's ported.
Account deletion done right: audit the cascade, don't trust the memo
TroubleshootJun 3, 2026 · 2 min
The App Store requires in-app account deletion. A leftover note claimed most user tables wouldn't cascade-delete — so I audited the real schema before writing a line. It was wrong: everything already cascades. The only real gap was audio files on disk, plus two bugs the test flushed out.
When the dev server became a fork bomb: a local incident that's a production failure in miniature
Tech retroJun 3, 2026 · 4 min
Bringing up the web + backend locally to test, the Next.js dev server spawned hundreds of worker processes, exhausted memory, and made fork() fail machine-wide — ps, curl, and pkill all stopped working. It wasn't a production bug, but it's the exact shape of one: a crash-loop with no backoff and no memory limit. Notes on the triage and the defenses that contain it.
Build-passes ≠ works: a 26-finding adversarial audit of the mobile app, and the bugs a green build hid
Tech retroJun 3, 2026 · 4 min
The Expo app's eleven screens all compiled and bundled but had never run on a device. A six-dimension, adversarially-verified audit found 26 real issues — including a hard launch blocker (no signup screen) and a core feature that was silently dead (the shadowing loop never looped). 24 were fixed.
Full parity: the last screen ports, and the whole web app now runs native on one shared core
UpdateJun 3, 2026 · 2 min
The preposition primer was the last web-only screen. With it ported, every screen in Mimi has a native counterpart built on @shadow-ai/core. A note on porting visual features: ship the content, defer the decoration.
Mobile hits active-learning parity: SM-2 review is ported — what 'done' actually means
UpdateJun 3, 2026 · 2 min
The spaced-repetition clip review — the last core learning screen — is now native. Mimi's whole active learning loop runs on mobile from one shared core. A note on why I call the learning surface 'done' rather than counting screens.
Mobile YouTube half, part 2: the in-app segment player — and why the loop is the feature
UpdateJun 3, 2026 · 2 min
The clip player is now in-app: a YouTube IFrame embedded via react-native-youtube-iframe, playing just the clip's segment and looping it for shadowing. The embed was easy; the re-seek-on-ended loop is the part that actually makes it a shadowing tool.
Mobile shadowing, complete: recording yourself — and the one place the shared client couldn't follow
UpdateJun 3, 2026 · 2 min
The mobile app can now record your shadowing take with expo-audio and upload it to your account. The whole shared API layer carried over to native unchanged — except the one multipart boundary where a web Blob and a React Native file URI genuinely differ.
Turning Mimi into a product it can charge for — an entitlement skeleton that doesn't touch a card
UpdateJun 2, 2026 · 3 min
Laid the groundwork for monetization toward an App Store launch: a payment-source-agnostic entitlement model where Mimi stores only 'is this user pro, until when' and a secret-gated webhook flips it — plus the Terms and Privacy pages a real service needs.
Going native: turning the Mimi web app into an Expo iOS/Android app — share the brain, rebuild the shell
Tech retroJun 2, 2026 · 3 min
Rather than wrapping the Next.js web app in a WebView (App Store rejection risk) or forking it (drift), I extracted the platform-agnostic core into a shared monorepo package and built a fresh Expo client on top of it. The web app migrated to the shared package with a near-empty diff via re-export shims.
Mimi: turning YouTube into daily English practice for developers — the whole build in one read
UpdateJun 2, 2026 · 3 min
A single-read overview of Mimi — a full-stack app that turns YouTube videos into spaced-repetition shadowing practice and drills the grammar, collocations, and prepositions that never come automatically. The product, the two halves, and the engineering decisions worth defending.
The monorepo pays off: the native drill is UI-only — the brain came from core
UpdateJun 2, 2026 · 1 min
The first real feature on the Expo app — a native Pattern Drill — reuses the web app's exact content, card-key format, session policy, and grading from @shadow-ai/core. Only the UI is new.
Mobile reaches Practice-half parity — and the port keeps getting cheaper
UpdateJun 2, 2026 · 2 min
The Expo app now has the whole Practice hub — pattern + collocation drills, the AI compose check, and the weak-spots dashboard — every screen a thin native shell over @shadow-ai/core. The drill half is at web parity; the YouTube shadowing half is the harder batch left.
Porting the YouTube half to mobile, part 1: split the hard feature by dependency
UpdateJun 2, 2026 · 2 min
The shadowing half of Mimi needs native video and audio, which is the genuinely hard part of the mobile port. So I split it by dependency: Library, Import, and clip detail — everything that's just a core API call — shipped first, with an honest open-in-YouTube placeholder where the native player will go.
Bringing the README to recruiter caliber — and deleting the claims the code didn't back
Tech retroJun 2, 2026 · 2 min
Rewrote Mimi's README to a deep, portfolio-grade reference modeled on a sibling project. The part worth recording is the verification: several security/infra claims copied from the template (presigned URLs, HTTP Range) weren't true for Mimi, and got caught by grepping each one against the actual code before committing.
The prod Dockerfile was missing yt-dlp — caught by building it before the deploy
TroubleshootJun 1, 2026 · 2 min
Prepping the AWS backend deploy, I built the production Docker image locally first. yt-dlp — which the YouTube import shells out to — wasn't in it. A months-old commit claimed it added yt-dlp to the prod Dockerfile, but the diff showed the install actually went into Dockerfile.dev. The image build caught a deploy that would have shipped a broken import feature.
Week of May 25, 202627 entries · 8 Update · 3 Troubleshoot · 13 Tech retro · 3 UX retro
영작 mode: from recall to production, by adding one method to the AI abstraction
Tech retroMay 31, 2026 · 2 min
The drills so far test recall — you reproduce a known model. The composition mode is the other half: you write your OWN sentence using a target, and the AI judges it and offers a better version. Built by adding a single low-level complete() to the existing provider abstraction rather than a parallel client.
Moving the drill streak off localStorage and onto the account
Tech retroMay 31, 2026 · 2 min
The pattern and collocation drills tracked the daily streak in browser localStorage — fine for a personal tool, but it's the exact line between 'my practice' and a product. Added a backend practice slice (one row per user, timezone-correct streak rules as domain behavior) and swapped the frontend to a TanStack Query hook with optimistic reps.
Three drill upgrades: in-session requeue, spoken models, and a weak-spots view
UpdateMay 31, 2026 · 2 min
On top of the spaced-repetition drills: cards you miss now reappear within the same session, the model sentence is spoken aloud so you can shadow it, and a weak-spots dashboard surfaces what you keep getting wrong — all from data the daily grade already collects.
Two honest gaps after a feature sprint: a cost-guard on the AI endpoint and frontend tests
Tech retroMay 31, 2026 · 2 min
Asked 'is there anything left to harden?', the honest answer was two real things, not busywork: the one AI-cost endpoint had no rate limit, and the frontend logic shipped this session had no tests. Both closed.
Spaced repetition for the drills — and why Leitner, not SM-2
Tech retroMay 31, 2026 · 2 min
The pattern and collocation drills reshuffled the whole deck every time, wasting reps on chunks you'd already nailed. Added per-card spaced repetition on the account: the daily drill now surfaces what's due plus a trickle of new cards. Picked a Leitner box system over SM-2 because the drill's two outcomes map to it exactly.
Hardening the async AI pipeline: timeouts, a bounded pool, graceful shutdown, and honest 4xx
Tech retroMay 30, 2026 · 3 min
A multi-agent audit of the codebase found a classic compound prod-hang: AI HTTP clients with no read timeout running on an unbounded @Async executor, plus a catch-all that turned client errors into 500s. Here's the fix and why each part matters.
Revert hardcoded AWS account ID in ECS task definition to ACCOUNT_ID placeholder
TroubleshootMay 30, 2026 · 2 min
A prior session had substituted the real 12-digit AWS account ID into 9 ARN/image/bucket references in ecs-task-definition.json; reverted to the ACCOUNT_ID placeholder before it reached git history and gitignored *.local.json for real values.
Chunk-by-chunk shadowing: learning to speak in breaths, not words
UpdateMay 30, 2026 · 1 min
A Chunks tab in the player reveals a sentence one sense-group at a time. You say each chunk out loud before the next appears — training the habit of speaking in chunks instead of translating Korean word by word.
Prepositions are two problems: a collocation chunk drill, and a Practice hub to hold it
UX retroMay 30, 2026 · 2 min
Trying to add a picture to 'made a living off lying' surfaced the real insight: prepositions split into spatial ones (a diagram works) and idiomatic collocations (no logic — learn the whole word+preposition as one chunk). Built a 101-chunk collocation drill, and folded the three standalone drills under one Practice hub to stop the nav from sprawling.
Daily Pattern Drill: forcing the frames you don't use into your mouth
UpdateMay 30, 2026 · 2 min
Not phrasal verbs — the sentence frames natives use without thinking ('as long as ___', 'I should have + p.p.', 'Have you ever…?'). A daily production drill: see the frame + a meaning, say the English out loud, check a model, mark it, repeat — with a day streak so it actually happens every day.
When the abstraction leaks: AiAnalysisClient returned ClaudeClient.AnalysisResult
Tech retroMay 30, 2026 · 2 min
The pluggable AI-provider interface returned a type nested inside one of its implementations — so the 'neutral' abstraction depended on Claude, and Gemini imported Claude to satisfy it. The fix, plus extracting the one service the codebase forgot.
Adding JWT revocation without a session store: a token_version claim
Tech retroMay 30, 2026 · 2 min
Stateless JWTs can't be revoked — so changing your password didn't actually log out a stolen token. Here's the token_version approach: one claim, one column, one indexed lookup per request, and real revocation.
Making the AI path observable: request-id correlation + Micrometer timers
Tech retroMay 30, 2026 · 2 min
The prod JSON logger declared requestId/userId fields that nothing ever filled, and the slowest path in the app — the AI call — had no metrics. Fixed both: a correlation-id filter and a Micrometer timer on the analysis call.
The full grammar curriculum: 82 patterns, authored in parallel, reviewed for accuracy
UpdateMay 30, 2026 · 2 min
The pattern drill got its real content: the foundational-but-always-confusing English patterns a Korean developer needs — the 'as' family, relative clauses, indirect questions, tense, conditionals, verb patterns, articles, and more. 82 patterns / 246 items, authored one-agent-per-category in parallel, then reviewed before merge.
Preposition fill-in drill: turning reading into recall
UpdateMay 30, 2026 · 1 min
A Drill tab on the prepositions page: blank out the preposition in a phrase, type it, get checked. Items come from both the curated primer and the learner's own mined clip phrases — so reading the diagrams turns into active recall.
Preposition spotlight: teaching the why, not the gloss
UpdateMay 30, 2026 · 2 min
A new learning feature: for every meaningful preposition in a clip, the AI explains the relationship it encodes (off of = source you lean on; into = transformation) in Korean — built to fix the habit of guessing prepositions by mapping Korean particles word-by-word.
A preposition study page: curated images + the ones you mined yourself
UpdateMay 30, 2026 · 2 min
Beyond per-clip preposition notes, a dedicated /prepositions page: a curated primer of 16 core prepositions taught by their spatial image, with the real examples you've mined from your own clips mapped onto each one.
Six quick wins from the audit: a forgeable prod secret, an open actuator, and LIKE wildcards
Tech retroMay 30, 2026 · 3 min
Second hardening batch: fail-fast on the dev JWT secret in prod, lock down actuator/Swagger, escape search wildcards, kill a Deck N+1, stream S3 recordings, and de-Claude a misleading field name. Small diffs, real risk removed.
Testing the parts that mattered: the AI pipeline, the default parser, and the isolation gates
Tech retroMay 30, 2026 · 2 min
16 source files had tests; 103 didn't. Rather than chase a coverage number, I tested the four things whose failure would hurt most: the async analysis pipeline, the default provider's parser, the SRS streak logic, and the per-user isolation boundaries.
Show the relationship, don't translate it: animated preposition diagrams
UX retroMay 30, 2026 · 2 min
The first cut of the preposition page was text cards full of Korean — wrong for an English-learning tool. Reworked it to animated SVG diagrams + English only: a ball moving into / over / through a box, so you feel the relationship instead of reading a gloss.
Rewriting the README for recruiters — and trusting code over my own planning docs
Tech retroMay 28, 2026 · 2 min
Rewrote the README for a North American recruiter/interviewer audience. Before writing a word I verified every technical claim against the code — and found my own planning docs (ROADMAP §0.4, PROGRESS) still list translation + 직독직해 as out-of-scope even though they shipped, have migrations, and have E2E specs. Treated code as the source of truth.
AI provider abstraction — Claude credit ran out, swapped to Gemini in one env var
Tech retroMay 27, 2026 · 1 min
Anthropic credit hit zero mid-session. Introduced `AiAnalysisClient` interface; `ClaudeClient` and a new `GeminiClient` both implement it, each gated by `@ConditionalOnProperty(name = "tubeshadow.ai.provider")`. Switching providers is now one env var. Gemini's free tier (1500 req/day) covers personal use indefinitely; operating cost dropped to $0/mo.
Decks (Anki-style clip grouping) + Review next-due toast
UpdateMay 27, 2026 · 2 min
Two user-perception fixes shipped together. (1) Anki-style decks: a new `decks` table, nullable `deck_id` on `clips` (`ON DELETE SET NULL` so clips fall back to Inbox), library sidebar, per-card move dropdown, deck filter on review. (2) After Easy/Good/Hard/Again, a toast surfaces the actual next-due date so the clip doesn't feel like it 'vanished' from the queue.
Gemini 2.5 Flash: thinking tokens silently truncated JSON output
TroubleshootMay 27, 2026 · 1 min
After switching to Gemini, every analysis came back as `GEMINI_PARSE_FAILED: Unexpected end-of-input` at the same column. Cause: Gemini 2.5 Flash burns 'thinking' tokens before emitting visible output, eating most of `maxOutputTokens: 800` and truncating the JSON mid-string. Fix: `thinkingConfig: { thinkingBudget: 0 }` and bumped output cap to 4096.
Prompt-engineering 직독직해: forcing English word order in Korean output
UX retroMay 27, 2026 · 3 min
Asking Gemini for 'Korean chunks paired with English chunks' produced grammatical Korean in natural Korean order — useless for shadowing practice. Three prompt revisions: (1) hard rules + bad examples to force source order, (2) tune chunk size from 1-word to 2–5-word sense groups, (3) explicit BAD examples that show the failure mode.
Recording upload 415: MIME `Content-Type` whitelist must strip codec parameter
TroubleshootMay 26, 2026 · 1 min
Chrome's MediaRecorder tags audio with `audio/webm;codecs=opus`. The recording upload service compared the full string against a whitelist of base types, so Chrome uploads were rejected with 415. Stripped MIME parameters before the check.
i18n: next-intl with path-based routing across 5 locales
Tech retroMay 25, 2026 · 2 min
Migrated the entire frontend to `next-intl` with path-based routing (`/en`, `/ko`, `/ja`, `/zh`, `/es`), default English. All 17 user-facing files were keyed; `<Link>` and `useRouter` imports across the app moved to `@/i18n/navigation` wrappers. ja/zh/es fall back to English copy for now (translation is a non-engineering task).
Week of May 18, 20264 entries · 2 Troubleshoot · 1 Tech retro · 1 Monetization
BYOAI — send analysis prompts to the user's own ChatGPT/Claude/Gemini
MonetizationMay 24, 2026 · 1 min
Built a 'Send to my own AI' button that constructs an analysis prompt locally and opens ChatGPT/Claude/Gemini/Perplexity with `?q=...`, falling back to clipboard if auto-fill fails. Operating cost: zero; user keeps the conversation in their preferred model. Sets up a clean alternative to a paid tier.
StaleObjectStateException on clip delete cascading to recordings
TroubleshootMay 24, 2026 · 1 min
Spring Data's derived `deleteByClipId` hydrates the entity into the persistence context before deleting it, racing with the DB-level `ON DELETE CASCADE`. Switched to `@Modifying @Query` so the delete bypasses the session cache.
AuthRateLimitFilter: Filter auto-registration trap → HandlerInterceptor
Tech retroMay 23, 2026 · 1 min
A `Filter` bean was auto-registered by Spring Boot independent of the `SecurityFilterChain` wiring and tried to instantiate it with a default constructor it didn't have. Switched to `HandlerInterceptor`; same behavior, no auto-registration trap.
YouTube transcript fetch replaced with yt-dlp + self-healing import
TroubleshootMay 23, 2026 · 1 min
The `timedtext` URL embedded in YouTube watch HTML carries a short-lived token; subtitle fetch returned 200 with empty body within minutes. Migrated entirely to `yt-dlp` subprocess and added an idempotent recovery hook so re-importing the same URL retries the transcript and the dimension probe.