What it is
A full-stack web app that frames Tokyo sightseeing as a quest game: users sign in, browse quests, save favorites, submit proof media to complete quests, earn EXP, and unlock story chapters as they level up.
Gamified Travel
A full-stack Next.js app that turns Tokyo sightseeing into a quest game — sign in with Google, complete quests with proof media, earn EXP, and unlock story chapters by leveling up.
Gamified Travel
Overview
A recruiter-friendly breakdown of the product goal, audience, and engineering direction.
A full-stack web app that frames Tokyo sightseeing as a quest game: users sign in, browse quests, save favorites, submit proof media to complete quests, earn EXP, and unlock story chapters as they level up.
Tokyo visitors and local explorers who prefer mission-based discovery over a static list of places.
Adding progress, completion proof, and a narrative layer turns one-off sightseeing into something repeatable and worth coming back to.
Problem
Most Tokyo travel apps are flat lists of places with no sense of progress or storytelling. Users have no reason to come back, and exploration feels detached from any larger arc.
Solution
Model the experience as quest → save → complete → level → unlock, then keep the integrity-critical pieces (auth, completion validation, level/story progression) on the server.
Architecture
A high-level system view that explains the app layers and data flow without hiding the engineering decisions.
Next.js 15 App Router with TypeScript, Tailwind CSS, and Chakra UI. Server components handle data loading and access control; client components drive quest cards, the camera/upload flow, the story timeline + reader modal, and the level-up modal.
Next.js Route Handlers under src/app/api/ for quests, saves, completions, reviews, stories, blogs, and admin operations. Zod validation, rate limiting, security headers (next.config.ts), and middleware-based auth redirects sit in front of every protected route.
PostgreSQL via Prisma. The schema models User, Quest, Tag, QuestCompletion, SavedQuest, Review, Blog, BlogContent, StoryChapter, StoryProgress, and StoryAnswer. A DTO layer (src/lib/dto.ts) limits what the client receives.
NextAuth.js with the Google provider, Prisma adapter, JWT sessions, and secure-cookie options. Middleware redirects unauthenticated users away from protected paths; admin APIs additionally enforce isStaff.
Targets a Vercel-style Next.js deployment (next.config.ts uses standalone output and @vercel/speed-insights). Live URL at tokyoquest.jp is referenced in code but not verified from the repo alone.
User submits a Zod-validated request with their NextAuth session.
Middleware enforces auth and applies security headers.
Route handler validates input (and media for completions) and runs Prisma calls.
Completion logic awards EXP, recomputes level, and updates story progress.
Pages map results through DTOs before rendering the client UI.
Features
NextAuth with the Google provider, Prisma adapter, JWT sessions, and secure-cookie options.
Authenticated /home feed with pagination, saved-state decoration, and tag-based category pages.
Optimistic UI on quest cards backed by a SavedQuest join model so users can plan a trip before completing anything.
Accepts Base64 image/video data, enforces type/size checks, prevents duplicate completions, awards 100 EXP, and recalculates level.
Story chapters unlock by user level, with read-progress tracking and persisted riddle answers (StoryProgress / StoryAnswer).
Internal tools for managing completions, quests, tags, blog content, and Supabase-backed image uploads, gated by isStaff.
Engineering
Problem
A single user action has to validate media, block duplicate completions, grant EXP, recalculate level, and update story progress — and none of those steps can be left half-finished.
Resolution
Centralize the flow in src/app/api/quests/[id]/complete/route.ts with Zod-validated payloads, server-side type/size checks on Base64 media, and a level-system module (src/lib/level-system.ts) that owns thresholds and unit-tested level math.
Problem
Prisma models include fields the client should never see (timestamps on internal joins, raw user metadata, admin-only flags), and exposing them by accident is the easiest way to leak data.
Resolution
Route every page through src/lib/dto.ts so home, profile, and quest detail pages render from explicit DTOs instead of raw Prisma results, and isolate admin reads behind isStaff-checked endpoints.
Problem
Stories aren't a separate feature — they have to react to quest completions in real time without becoming a tangle of cross-cutting writes.
Resolution
Treat StoryProgress.unlockedLevels as the single source of truth, update it from the completion route, and let the story APIs and StoryReader gate chapters and persist read state / riddle answers off that one field.