Full Rebuild of the Guest Mobile App with Next.js 16 — Research-Based UX Design and the Bugs We Found Along the Way
We completely rebuilt the old React (Vite) app with Next.js 16 App Router. We used recent UX research such as Hick's Law, Choice Overload, and Progressive Disclosure as design guidelines, and combined security and speed with Server Components plus an httpOnly cookie + API proxy. In parallel, we fixed 3 server bugs hidden in the old code.
We fully rebuilt the guest mobile app (guest.no-tel.com) with Next.js 16 and a design based on recent UX research. It is a complete replacement of the React (Vite) app that had been running since 2023. In this article, we summarize the technology stack and design decisions we chose, plus the server bugs we found in the old code during the rebuild and how we fixed them.
Why a rebuild was necessary
The old guest mobile app was developed by an external contractor, and it had been running for a long time in a state where we could not take over the source code.
- No access rights to the GitHub repository (it was tied to the contractor's account)
- Only the minified bundle was deployed to the production S3 bucket
- The reverse-engineered source on EC2 was incompletely restored
Continuing incident response and feature development in this state was not realistic, so we decided to rebuild from zero.
Technology stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| UI | Tailwind CSS 4 + shadcn/ui (radix-nova preset) |
| Validation | Zod + react-hook-form |
| State management | TanStack Query v5 + React state |
| Authentication | JWT (httpOnly cookie) + profile cookie |
| Deployment | Vercel (separate Production / Preview) |
Authentication architecture: httpOnly cookie + API proxy
To prevent JWT leaks through XSS, the JWT is stored in an httpOnly cookie. However, client-side JavaScript cannot read that cookie, so the client cannot call the backend API directly. Our solution is a 3-layer structure:
Client → Next.js API Route (proxy) → 上流 API (api.no-tel.com)
↑ ここで cookie から JWT を取り出して Authorization ヘッダーに載せる
(The proxy reads the JWT from the cookie and puts it into the Authorization header.)
The proxy lives under app/api/... and also plays a BFF-style role such as response shaping and shopIds filtering.
For SSR authentication, we prepared the getProfileCookie() / apiFetch() utilities so that they can be called directly from Server Components.
Research-based UX design — 3 pillars
Instead of designing by feeling, we based our design decisions on existing research.
1. Hick's Law: decision time grows logarithmically with the number of options
In venues where more than 20 cast members are working on a given day, listing everyone causes an explosion of scrolling and cognitive load (Laws of UX).
- Show 5 items at first (the optimal range of 3–5 options from Hick's Law)
- A "show more" button for step-by-step disclosure (Progressive Disclosure)
- Favorite cast members come first → zero initial cognitive load for repeat visitors
2. Choice Overload: too many options pushes up drop-off rates
According to the Baymard Institute, the cart abandonment rate is 69.57%, and one of its main causes is too many options (Baymard).
Countermeasures:
- Categorization: automatic sorting by priority — favorites → most recent → available slots
- Search box: shown when there are more than 5 people, with incremental filtering by name
3. Progressive Disclosure: show information only when it is needed
Favorite cards are designed to not fetch shift information until they are tapped.
初期: 名前 + 店舗名のみ (軽い)
タップ: /api/favorites/shifts 呼び出し → 1 週間シフトをアコーディオン展開
シフトあり日タップ: 予約画面へ
(Initial state: name + venue name only (lightweight). Tap: calls /api/favorites/shifts → expands one week of shifts in an accordion. Tapping a day with a shift: goes to the reservation screen.)
This reduces API requests on the order of users × cast members, which also lowers server load.
Speed optimization — from a noticeable 3 seconds to much faster
Pinning Vercel functions to the Tokyo region (hnd1)
In the initial deployment, the calendar screen took 3 seconds to show. Profiling revealed that the Vercel serverless functions were running in Washington D.C. (iad1), paying nearly 200ms of RTT on every round trip to the API server in Tokyo.
// vercel.json
{
"regions": ["hnd1"]
}
This single line reduced the RTT to roughly 1/10, and the perceived speed of other pages also improved a lot.
Client-side caching makes date switching instant
If switching dates in the calendar triggered an API request every time, the experience would degrade by 3 seconds × 7 days. So we implemented a session-scoped cache with useRef(new Map()):
const cacheRef = useRef(new Map<string, Data>());
const fetchDate = useCallback(async (date: string) => {
const key = `${shopId}-${date}`;
const cached = cacheRef.current.get(key);
if (cached) {
setData(cached);
return;
}
// ... fetch + cacheRef.current.set(key, data)
}, [shopId]);
Returning to a previously viewed date now shows instantly, giving a smooth experience.
Business day switches at 6:00
Late-night hours (00:00–05:59) should be treated as "the previous day's business", but the calendar switched dates at midnight, which caused a problem where late-night reservation slots disappeared.
When we aggregated changeDateTime across all companies, most venues switched between 03:00 and 09:00, so we standardized on a uniform 06:00 switch:
export function getBusinessToday(): Date {
const now = new Date();
if (now.getHours() < 6) now.setDate(now.getDate() - 1);
now.setHours(0, 0, 0, 0);
return now;
}
3 server bugs that had been left in the old code
During the rebuild, we found API bugs that had been sitting in the old code, and fixed them. In every case, the feature was either not working at all, or working only in a limited way.
Bug #1 — Notifications with start_date NULL were all excluded
// 旧コード (notification/service.ts)
.andWhere("notifications.start_date < NOW()")
Most notifications are created with start_date = NULL (publish immediately), and NULL < NOW() is always false → every row was filtered out.
This was the reason notifications never appeared in the old guest mobile app.
// 修正後
.andWhere("(notifications.start_date IS NULL OR notifications.start_date < NOW())")
Bug #2 — Inverted logic in the read-status API
// 旧コード (notification/service.ts:isRead)
const createNotificationReadIds = body.filter((id) =>
notificationReads.map(n => n.notificationId).includes(id) // ← 反転
);
It tried to register again the items that were already marked as read, while unread items were never registered — a complete inversion.
// 修正後
const alreadyReadIds = new Set(notificationReads.map(nr => nr.notificationId));
const createNotificationReadIds = body.filter((id) => !alreadyReadIds.has(id));
Bug #3 — Reserved slots not excluded from the calendar
findOrderCalendar filtered only on status: [paid, booking], so reservations in "requested" (guestRequest) and "proposed" (guestConfirm) states were shown as open slots.
We also fixed, at the same time, a bug where an empty castNameIds array produced a SQL error (500) through WHERE IN ():
status: [
OrderStatus.paid,
OrderStatus.booking,
OrderStatus.guestRequest,
OrderStatus.guestConfirm,
]
// castNameIds が配列空のときは条件追加をスキップ
(When castNameIds is an empty array, the condition is skipped.)
New feature highlights
Reservation request flow (proposal-based)
We unified the flow as follows: the user selects a date/time, cast member, and course and submits a request → venue staff review and edit the content → send it back as a proposal → the user approves and the reservation is confirmed. This gives us:
- Flexible handling of per-venue rules (options / extension fees / minimum usage time) that match real operations
- The user only needs to wait after submitting; input mistakes are corrected by the venue
TOP status cards
On the home screen we placed cards in 3 colors: "Requested", "Proposal available", and "Confirmed". In particular, "Proposal available" is highlighted in coral, and tapping it goes straight to the details.
Unified accordion for favorites and history
Favorite cast cards and usage-history cards now share a common UI: tapping a card expands one week of shifts in place. It answers the question "When are they available?" without a page transition, and tapping a day with a shift takes you directly to the reservation calendar.
Points screen
Per-venue balance plus a note that points are "shared within the group". Because we received many inquiries like "I have points, but why are they different per venue?", we reviewed the data structure and separated perCompany and perShop.
An independent QA environment
In the new implementation, the QA environment is fully independent.
- Vercel Preview (develop branch) → QA API server → QA DB
- Vercel Production (main branch) → production API → production DB
This lets us test with zero impact on production. Payment testing with the 4242 test card is also possible in the QA environment.
Rollback path to the old app
If a problem occurs, we can immediately return to the old app by pointing the Route53 CNAME back to the original CloudFront (the old S3 bucket is also kept). The server-side query fixes are backed up per file (/tmp/*-backup-{timestamp}.ts), so a rollback with a small blast radius is possible.
References
- Hick's Law - Laws of UX
- Choice Overload - Laws of UX
- Progressive Disclosure - NN/g
- Baymard: Cart Abandonment Statistics
- Booking UX Best Practices 2025 - Ralabs
- Load More Pattern - UX Patterns
What comes next
- The remaining α-4 features (push notifications, LINE integration) are planned for the β phase
- For the
orderstable (around 1.29 million rows) and thecast_shiftstable (around 290 thousand rows), an archive strategy is being prepared as a separate project (most recent 1 month + separated history tables) - MFA support for the authentication platform is also under consideration
At tasteck, we will keep strengthening our UX and infrastructure in a research-based, data-driven way.
Related articles
How We Took LINE Away from the AI Dev Team and Centralized It in a Business-Analysis AI — Layering an Org Hierarchy on Top of Flat claude-peers (Build-in-Public Day 26)
A design decision that layers an organizational hierarchy, via operational policy, on top of the flat communication of the claude-peers MCP. LINE group access is granted only to the business-analysis AI and taken away from the dev AI. We explain a multi-agent operations pattern that reproduces noise filtering, layered responsibility, and symmetry with human organizations, with AI as the actors.
DB Performance Improvement Phase 1 — Moving from Ad-hoc Fixes to Measurement-First Operations
We added 7 indexes to the tasteck DB, which includes an orders table with 1,424,125 rows. Key queries such as the guest calendar, the incoming call history list, and point balance calculation were verified with EXPLAIN to use range scans, and everything was applied to production with no downtime. At the same time, we re-read the database evolution write-ups from Shopify, GitHub, Notion, and Figma, and we share the background of our decision to graduate from 'symptom-driven ad-hoc fixes' and switch to 'measurement-first' operations.
Map out your operations in 5 minutes
Eight questions cover reservations, customer management, shifts, and settlement. Results shown instantly with industry benchmark. Sales emails only if you request them.
Your answers are not stored. The assessment runs entirely in your browser.
Try tasteck free for 30 days
No credit card required. Full access to reservations, cast shifts, dispatch, and analytics.
- No card required
- Free data migration support
- All features unlocked for 30 days