TorontoGuessr is a Toronto-only Street View guessing game with a Next.js frontend, a small Node backend, and Supabase for persistence.
Players get five Toronto Street View locations, place guesses on a map, and earn points based on geographic accuracy. The app also includes leaderboards, daily gameplay stats, SEO metadata routes, and an admin review tool for manually approving or rejecting cached Street View locations.
frontend/: Next.js 15 App Router appbackend/: Small Node API used locally and in Vercel Functions- Supabase: Verified location cache, game sessions, leaderboard data
- Google Maps / Google Street View: gameplay map, Street View rendering, review tooling
.
├── frontend/
├── backend/
├── scripts/
└── package.json
- Node.js 20+
- npm 10+
- A Supabase project
- A Google Maps API key with the APIs your app uses enabled
-
Install dependencies:
npm install
-
Copy the env examples:
cp backend/.env.example backend/.env cp frontend/.env.example frontend/.env
-
Create a Supabase project.
-
For a fresh database, run backend/supabase/schema.sql in the Supabase SQL editor.
This schema enables Row Level Security (RLS) on the app tables. The backend uses the Supabase
service_rolekey, so it continues to work while anonymous access stays blocked by default.If you already had an older TorontoGuessr schema, also run backend/supabase/add_verified_location_review_columns.sql to add the manual review fields.
If Supabase is warning that RLS is disabled on an existing project, also run backend/supabase/enable_row_level_security.sql.
-
Fill in
backend/.envusing backend/.env.example:PORT=3001 SUPABASE_URL=https://your-project-ref.supabase.co SUPABASE_SERVICE_ROLE_KEY=your-service-role-key LOCATION_GENERATION_ENABLED=false NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key ADMIN_REVIEW_TOKEN=your-long-random-admin-token
-
Fill in
frontend/.envusing frontend/.env.example:NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api SITE_URL=http://localhost:3000 NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key
-
Start the app:
npm run dev
Frontend runs on http://localhost:3000 and the backend runs on http://localhost:3001.
For normal app usage, the backend needs:
SUPABASE_URLSUPABASE_SERVICE_ROLE_KEY
You can find both in the Supabase dashboard:
SUPABASE_URL: project dashboard ->Connect->Project URLSUPABASE_SERVICE_ROLE_KEY:Settings->API Keys->service_role
Use the project URL itself, for example:
SUPABASE_URL=https://your-project-ref.supabase.coDo not use NEXT_PUBLIC_SUPABASE_* variables here. This app talks to Supabase from the backend, not from the browser.
Because the public tables have RLS enabled, the backend should use SUPABASE_SERVICE_ROLE_KEY, not an anon key.
The Supabase schema creates two main tables:
verified_locations: cached Toronto Street View locations withlat,lng,pano_id,manually_verified, andreview_statusgame_sessions: persisted game state, results, and finished sessions used for leaderboards and stats
Notes:
manually_verified = truemeans a location was explicitly approved in the admin review tool.review_statusis one ofpending,accepted, orrejected.- Gameplay prefers manually verified locations first and excludes rejected ones.
- Leaderboards are derived from finished
game_sessionsrows.
Main gameplay behavior:
- each game starts a five-round session
- rounds are selected from cached verified locations in Supabase
- if generation is enabled and the cache is too small, the backend can generate more verified Toronto locations using Street View metadata
- scores are based on guess distance from the true location
Leaderboard behavior:
lifetime,daily,weekly, andmonthlyperiods are supported- the frontend shows a top-5 preview first
lifetimeexpands to a top-25 viewdaily,weekly, andmonthlykeep pagination after the preview
The admin review page lives at /admin/review-locations.
It is designed to manually filter out bad Street View cache entries, especially indoor or unusable panoramas. The review UI includes:
- Street View panorama preview
- 2D map preview with a marker on the saved coordinates
Accept,Reject,Previous,Next, andUndo Last Action
Review behavior:
Accept: setsmanually_verified = trueandreview_status = acceptedReject: setsreview_status = rejectedand removes the row from the active review queueUndo Last Action: restores the most recently accepted or rejected row topendingand returns the queue to that specific location
Admin access is protected by ADMIN_REVIEW_TOKEN, which must be sent to the backend through the review UI.
If you already migrated verified locations into Supabase, you can skip this section.
The one-time migration script is backend/scripts/migrate-verified-locations-from-firestore.mjs.
What it does:
- reads Firestore
verifiedLocations - validates existing coordinates against Google Street View when needed
- inserts verified rows into Supabase
verified_locations - does not generate new random coordinates
To run it:
-
Temporarily install Firebase in the backend workspace:
npm install --prefix backend --no-save firebase
-
Add the Firebase env vars shown in backend/.env.example to
backend/.env. -
Run the migration:
npm run migrate:verified-locations --workspace backend
-
After Supabase is seeded, keep this in
backend/.env:LOCATION_GENERATION_ENABLED=false
With generation disabled, the backend will not create new random Toronto coordinates at runtime.
There are now two ways verified locations can be added:
- Runtime generation during gameplay if
LOCATION_GENERATION_ENABLED=true - Manual bulk generation with backend/scripts/generate-verified-locations.mjs
To generate 100 more verified locations:
npm run generate:verified-locations --workspace backendTo generate a custom number:
npm run generate:verified-locations --workspace backend -- 25The script validates Street View coverage, skips duplicate panoramas already stored in Supabase, and inserts only newly verified rows.
The backend exposes a gameplay stats endpoint at GET /api/stats/games.
It returns:
- total games started
- total games finished
- a daily time-series for the requested range
Query parameters:
days: optional,1-365, defaults to30timeZone: optional, defaults toAmerica/Toronto
The frontend also includes Next.js metadata routes for:
robots.txtsitemap.xml
These use SITE_URL when available. The /game route is excluded from crawling because it auto-starts a game session.
From the repo root:
npm run dev: start frontend and backend togethernpm run build: build-check backend and frontendnpm run start: start frontend and backend in production mode
Workspace-specific:
npm run dev --workspace frontendnpm run dev --workspace backendnpm run generate:verified-locations --workspace backendnpm run migrate:verified-locations --workspace backend
Main backend routes:
GET /api/healthPOST /api/games/startPOST /api/games/:sessionId/guessPOST /api/games/:sessionId/nextPOST /api/games/:sessionId/usernameGET /api/leaderboard?period=lifetime|daily|weekly|monthly&page=1&limit=10GET /api/stats/games?days=30&timeZone=America/TorontoGET /api/admin/review-locations?index=0GET /api/admin/review-locations?locationId=<uuid>PATCH /api/admin/review-locations/:locationIdwith{ "action": "accept" | "reject" | "undo" }
This repo is easiest to deploy as two Vercel projects.
- import the same Git repo into Vercel
- set the root directory to
backend - framework preset:
Other - build command:
npm run build
Backend env vars:
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
LOCATION_GENERATION_ENABLED=false
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key
ADMIN_REVIEW_TOKEN=your-long-random-admin-tokenNotes:
NEXT_PUBLIC_GOOGLE_MAPS_API_KEYon the backend is only needed if the backend still performs Street View metadata lookups for migration, generation, or pano backfill.- If
verified_locationsis already fully seeded withpano_idvalues and generation is disabled, the backend can usually run without that key.
- import the same Git repo again
- set the root directory to
frontend - framework preset:
Next.js
Frontend env vars:
NEXT_PUBLIC_API_BASE_URL=https://your-backend-project.vercel.app/api
SITE_URL=https://www.torontoguessr.ca
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-keyDeploy order:
- deploy the backend project first
- copy its production URL
- set
NEXT_PUBLIC_API_BASE_URLin the frontend project - deploy the frontend project
The frontend Google Maps JavaScript key should allow the domains you actually use, for example:
https://www.torontoguessr.ca/*https://torontoguessr.ca/*http://localhost:3000/*
If you use preview deployments, also add the preview domain pattern.
Right now the repo can use the same Google key on both frontend and backend, but a cleaner long-term setup is:
- browser key for frontend Maps / Street View UI, restricted by HTTP referrer
- server key for backend Street View metadata requests
- Firebase is no longer used for normal runtime.
- Old Firestore credentials are only needed for the one-time migration script.
- The frontend still needs a Google Maps API key even after the Supabase migration because gameplay and review tooling both use Google Maps and Street View.