A subway car crowding mini-app
Kankan
Summary
The route is already known. The missing decision is which car to board. Enter where you get on and off, and one screen shows how crowded each car is at that hour and which car is closest to your transfer.
- Passed · live
- 77
- 456
Overview
The route is already known. The missing decision is which car to board. Map apps stop at the fastest route, so the car with a seat is picked on instinct every time. It started with my own commute.
Kankan is a mini-app that runs inside Toss (Apps in Toss). I planned, designed, built, submitted and operate it on my own, and it passed review and shipped.
Live: kankan-eta.vercel.app
Problem
The problem fits in one sentence. Which car do I board to get a seat?
- Finding a seat is guesswork – daily riders rely on instinct to decide where to stand.
- Map apps don't look at cars – Naver and Kakao Maps show the shortest or best route, not crowding by car.
- Who it's for – commuters and students who want a seat but have no information, and stand again today.
Key decisions
Answer before the train arrives – one input, one result
For someone deciding seconds before boarding, tabs, settings and onboarding all come too late.
A single-screen flow: enter two stations and the result appears. Station search works with Korean initial consonants.
Finish the transfer on the same screen
If the car with a seat and the car nearest the transfer differ, people end up opening another app.
Each leg shows crowding by car, and the transfer step shows the quick-transfer car number.
Read it by colour, not numbers
Someone waiting on a platform has no time to read, and no time to doubt the screen.
Crowding is coded green, yellow and red, with the percentage secondary. The recommended car is marked.
Don't trust public data as it is
Checking quick-transfer data station by station against an existing route app turned up missing and wrong entries. A wrong car number becomes a wrong move.
Cross-checked the Ministry of Land data against the Seoul Metro API. The 77 records where they disagreed were checked and corrected by hand (measured).
A verified static bundle beats an uncertain live value
The live API was unreliable in places. Not shipping a wrong car number mattered more than showing the newest value.
456 verified quick-transfer records ship with the app as static data (output). No live call is made for them.
Process
Designing within constraints
I mapped the platform, infrastructure and API limits early, and let each one shape a design or build decision.
- Apps in Toss policy – no pop-up on entry, no pushing users to outside apps, no blocking the back action. The back button was removed in favour of the native gesture. The app passed review once those conditions were met.
- WebView – built to the Apps in Toss SDK and web framework guide, entirely inside a WebView with no native code.
- Free API limits – crowding doesn't change much, so it is collected in batches and cached in Supabase, keeping live calls within the 1,000-a-day limit. Running cost stays at zero.
Design
Toss Design System (TDS) components are used as they are, so the app feels native inside Toss.
The TDS components used
- SearchBar – station search.
- Badge – crowding, colour-coded.
- CTAButton – "Recommend a car", the one action.
- NavigationBar – logo and centred title.
Data
Sources and processing
- Crowding and alighting ratio by car – Puzzle API (Geovision SK), in 30-minute steps, lines 1–8. Collected in batches, cached in Supabase and read by hour.
- Quick-transfer car numbers – Ministry of Land CSV and the Seoul Metro API. Cross-checked, 77 errors fixed, 456 records bundled as static data.
- Maintenance – a batch script refreshes crowding in Supabase; when station data changes, quick-transfer data is re-checked and replaced. API usage is watched in Vercel logs.
Results
Shipping is the result. It passed review and runs inside Toss. Usage is not measured yet.
- Passed · live
- 77
- ₩0
Where a person steps in
- By itself – public data is cross-checked between two sources and baked into a cache.
- By hand – the 77 places where the two disagreed were checked and corrected by a person, and where the live API is unreliable, 456 verified records ship as static data so the app never shows a wrong number in real time.
Reflection
What held up
- The same data becomes a different service in a different context. The crowding data already existed; fitted to the moment before boarding, it became something else.
- Choosing not to ship a wrong value. Verified records won over fresh ones. Coverage stops at lines 1–8, but no wrong car number reaches the screen.
- Removing the cost was a design decision too. To keep a solo-built service open for long, it had to run at zero.
What failed
- I assumed two public sources would agree. They disagreed on 77 records. Trusting one of them would have shipped wrong car numbers.
- I assumed live values were always better. The live quick-transfer data did not match reality. Being right came before being current.
What I would test next
- Whether the recommendation was right. There is no way yet to tell whether people got a seat in the car Kankan suggested.
- Crowding is a 30-minute time-slot figure. How far it drifts from a given day's reality is unknown.
- More lines – whether the same data quality exists beyond lines 1–8. Line 9 and the metropolitan express lines (GTX) are next.
- Alerts – whether a push before departure beats deciding in the seconds before boarding is still unknown.