A city-exploration game platform — turn an unknown city into a quest list, complete tasks on the move, score points
In development since March 2026
What it is
CityGame turns the city around you into a game board. You open the mobile app in a place you've never explored, the platform proposes a series of geo-anchored challenges — visit a square, photograph a monument, find a coffee shop the platform has marked — and rewards completion with points, badges, and progression on a shared leaderboard.
The product runs as four separate applications inside one Turborepo monorepo, each speaking to the same backend over a shared types package, so a contract change in one place propagates with TypeScript errors everywhere it lands.
The four apps
The repo is organised under apps/ with one folder per deployable artefact and packages/shared carrying the types each app imports. Turborepo pipelines builds, type-checks, lints, and tests across every app from a single command.
| App | Stack | Role |
|---|---|---|
apps/backend |
NestJS 10, Prisma, PostgreSQL, Socket.IO, JWT | API + real-time game server |
apps/mobile |
Expo 54, React Native, React Navigation, React Query | Player-facing iOS / Android app |
apps/admin |
Next.js 15, React 19, Tailwind v4, React Hook Form + Zod | Operator dashboard for level design and live ops |
apps/landing |
Astro 5 | Marketing site with screenshots and download links |
packages/shared |
TypeScript only | DTOs, enums, and Zod schemas shared by every app |
The split is deliberate: the mobile app is the player surface, the admin is where game-master content gets authored, the landing page is for acquisition, and the backend is the single source of truth they all talk to.
Backend
The NestJS backend exposes a REST API for everything that's transactional — game registration, challenge completion, photo upload, score updates — and a Socket.IO gateway for everything that needs to feel alive: when one player completes a challenge, every other player in the same session sees the leaderboard update in real time.
Authentication uses JWT with refresh-token rotation. Player profiles, game sessions, challenges, completions, and uploaded photos live in PostgreSQL behind Prisma. Photos are uploaded directly to S3-compatible storage with presigned URLs, so the backend never streams the actual file bytes — it only signs the upload and records the resulting URL on the completion row.
Mobile
The mobile app is built with Expo 54 on the New Architecture. Maps use react-native-maps overlays with custom markers for active challenges. State management is React Query for server state, with light Zustand for client-only UI state.
The flow is intentionally simple: open the app, see the map, tap a challenge, walk to the location, complete the action, get a point. No multi-step onboarding, no gamification overload at sign-up — just a clear loop the player can grok in thirty seconds.
Admin
The admin panel is where the game design happens. It runs on Next.js 15 with React 19, Tailwind v4, and uses React Hook Form + Zod schemas (imported from @citygame/shared) so form validation matches the backend contract exactly.
Operators can:
- Author new challenges with map-anchored coordinates picked from a Leaflet preview
- Upload reference photos and tweak point values
- Schedule games (start time, end time, max participants)
- Watch live participation through a Socket.IO subscription, the same one the mobile app uses
- View per-player progress charts via Recharts
🛠️ Tech Stack
| Category | Stack |
|---|---|
| Backend | NestJS 10, Prisma, PostgreSQL, Socket.IO, JWT, Passport, AWS S3 |
| Mobile | Expo 54, React Native, React Navigation 6, React Query, Leaflet |
| Admin | Next.js 15, React 19, Tailwind v4, React Hook Form, Zod, Recharts, Lucide |
| Landing | Astro 5, Tailwind v4 |
| Tooling | Turborepo 2, pnpm workspaces, TypeScript 5.7, ESLint 9, Prettier |
Why a monorepo
The shared-types package is the answer. Three TypeScript apps and one TypeScript backend that all describe the same player, the same challenge, the same completion event. Without a shared package they would drift; with one, a backend type change becomes a compile error in the admin form three apps over.
Turborepo's caching means a no-op CI run finishes in seconds. A backend-only PR doesn't trigger mobile or admin rebuilds; a shared-types change triggers all four. The dependency graph drives the test suite, not vibes.
Status and what's next
The four apps run end-to-end on a development box. The backend deploys to a managed PostgreSQL host; the admin deploys to Vercel; the landing is static-built and CDN-served; the mobile app builds on EAS and ships through TestFlight for beta testers. Active work is around live-ops tooling — pause/resume games mid-flight, push notifications for nearby challenges, and an in-admin replay viewer that scrubs through a session's Socket.IO event stream.
CityGame is a four-app Turborepo monorepo for city exploration gameplay. Backend in NestJS 10 with Prisma, PostgreSQL, Socket.IO and JWT; mobile in Expo 54 with React Navigation and Leaflet maps; admin in Next.js 15 with React 19 and Tailwind v4; landing in Astro 5. Highlights include real-time game state via WebSockets, geographic challenges, an admin panel for level design, and a clean shared-types package. Built with NestJS, React Native, Next.js, Astro, Prisma, Socket.IO, TypeScript, and Turborepo. In development since March 2026.