Case study / Web application
UnTangler
A voice-first app that helps students with ADHD turn an overwhelming task into small steps, then puts those steps on their Google Calendar.
The problem
UnTangler is for students with ADHD who are facing a task that feels too big to start.
What it had to do
- Turn that task into small steps, scheduled where the student already looks
- Make adding a task as easy as saying it out loud
- Land a spoken time such as “tomorrow at 5” on the right day, in the student’s own timezone
- Reach the phone with the reminder, including an iPhone with the app installed
What was built
An installable web app (a PWA): a React and TypeScript front end, an Express API and a Supabase Postgres database.
How it works, step by step
- The student says a task out loud and refines it by talking, with no restart.
- Spoken dates and times are resolved in the student’s real timezone, and can be corrected by hand.
- The steps sync to Google Calendar, Tasks and Classroom. Finishing a step is written back to the Calendar event.
- An end-of-day check-in asks what got done. Reminders arrive as push notifications and in an in-app bell.
My part
- The Gemini Live voice-to-schedule pipeline and the timezone-aware date handling.
- Google Calendar, Tasks and Classroom sync, including the stuck and broken-connection cases.
- The whole end-of-day check-in flow and its notifications.
- Installable app behaviour on iOS and Android, an app-wide accessibility audit fixed to WCAG AA, and the CI workflow.
The rest of the team
One other developer and a product owner, who writes the specs in plain language. Every change of mine is reviewed in a pull request before it merges.
What I built, in detail
Voice
Speak a to-do, refine it by talking
- Gemini Live: say a task and adjust it in conversation, with no restart
- Short-lived, single-use, model-locked tokens issued by the backend
- No WebSocket proxy: the backend is one Vercel serverless function
Dates
Spoken times that land on the right day
- A timezone-aware converter from wall-clock time to UTC
- The student’s real timezone threaded through every date-resolution path
- A resolved due date can be corrected by hand
Google
Calendar, Tasks and Classroom sync
- Task completion written back to the real Calendar event
- Sync states that got stuck silently, fixed
- An unreadable Google connection is recovered from, not a crash
Check-ins
The end-of-day check-in
- Finish-time entry, snooze, daily confirmation and catch-up
- Web Push and an in-app notification bell
- A scheduled job (GitHub Actions and a Supabase cron migration) that dispatches due check-ins
PWA
Installable on iOS and Android
- iOS and standalone-mode detection, install metadata and an install option in the app
- Fixes for service worker and manifest caching problems
- Microphone permission fixed in the installed iPhone app, and a mic that connects but never captures audio is detected
- A real-browser end-to-end suite for iOS and Android push
Accessibility
An app-wide audit, fixed to WCAG AA
- Very Major, Major and Minor findings resolved
- WCAG AA colour contrast, ARIA landmarks and touch-target sizes
- aria-live and role=alert on async status text
- Consistent focus and disabled states
Reliability
Security and stability
- The Live token endpoint requires authentication
- Voice sessions are guarded against re-entry
- Supabase calls are wrapped in error handling
- Flaky tests fixed: a time-dependent scheduling test and an extraction test that made real network calls
Delivery
CI, deploys and migrations
- Set up the CI workflow
- Sped up deploys by installing frontend and backend dependencies in parallel
- Fixed production build failures
- Schema changes go through versioned Supabase migrations
Decisions that shaped it
The browser talks to Gemini directly
The backend hands the browser a token that is short-lived, single-use and locked to one model. There is no WebSocket proxy to run or pay for, and the real API key never leaves the server.
Reminder times live in the database
Saved per user, not held in the server’s memory. A value in memory is shared by everyone and disappears whenever a serverless function restarts.
A broken Google connection is something to recover from
The app treats it as a state to repair and tells the student, instead of crashing or leaving a sync silently stuck.
The outcome
UnTangler is in production and I am still building it. Every feature above shipped through a reviewed pull request.
- non-merge commits are mine
- 160 of 286
- pull requests merged
- ~190
- database migrations
- 19
- test files
- 77
- CI workflows
- 2
- Jira tickets assigned to me are done
- 88 of 89
From the project’s git history and Jira board on 17 September 2026, covering 17 July to 17 September. Both are private, so there is no public link to check them against.
How I work with the team
01
Specs become software
I work from the product owner’s plain-language specs and break each epic into stories and subtasks. I am the reporter on 128 of the project’s 181 tickets.
02
Ask before building
On the weekly summary feature I posted my starting plan and asked for a decision first. When a ticket leaves out a priority or an estimate, I ask instead of guessing.
03
Two audiences, two registers
Technical status goes to the engineers. A plain-language note closes the ticket for the product owner: what changed for the student, not which files.
04
Own the gaps
On a push-reliability ticket I wrote that my first comment only covered half the spec, then reported the rest. I also flagged and fixed a gap my own cleanup could cause.
05
Small, reviewable changes
One pull request holds one coherent change, on a branch off main, with a named reviewer.
What you can check
- Employer and product
Compass Decisions Science LLC and UnTangler are named with the company’s permission.
- The numbers
The repository and the Jira board are private, so the figures cannot be linked. They were read on 17 September 2026.
- What is left out
Colleague names, ticket numbers and internal quotes, on purpose.