Turn geographical searches into historical and cultural discoveries.
Landing page with debounced location search via the Photon API (OpenStreetMap). Selecting a location opens a full-screen Mapbox GL map centered on that place, with pan and zoom.
When you select a location, the app enqueues a background job instead of calling Gemini on the request thread:
POST /api/lorestores a short-lived job in Upstash Redis and publishes to Upstash QStash.- QStash delivers work to
/api/cron/process-lorewith flow control (15 requests/minute, 5 parallel max) to protect the Gemini free tier. - The worker fetches Wikipedia articles and runs one
generateTextcall per job. - The map polls
GET /api/lore?jobId=…every 2s until pins are ready.
Completed lore jobs are persisted to Supabase (one row per Wikipedia page_id). The map loads cached cards in the current viewport as small dots; zoom in to see headlines (zoom ≥ 14). Supabase is optional for local dev without env vars.
API quota: Each location search uses one Gemini generateText call (Wikipedia is fetched separately, no AI) on LORE_MODEL_ID (default gemini-3.1-flash-lite). On 429 / rate limit, the job stays on that model and QStash retries later. On 503 / high demand, the worker retries with backoff, then tries LORE_MODEL_FALLBACK_IDS in order (gemini-2.5-flash, then gemini-2.5-flash-lite by default). 403 errors fail immediately (check API key / billing).
- Node.js 20+
- A Mapbox account with a public access token (map display only)
- A Google AI API key for the lore agent
- Upstash QStash and Upstash Redis (free tiers)
- Supabase project with PostGIS enabled (geographic lore cache)
-
Install dependencies:
npm install
-
Copy the environment template and fill in values:
cp .env.example .env.local
Required in
.env.local:NEXT_PUBLIC_MAPBOX_ACCESS_TOKENGOOGLE_GENERATIVE_AI_API_KEYQSTASH_TOKEN,QSTASH_CURRENT_SIGNING_KEY,QSTASH_NEXT_SIGNING_KEYUPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKENLORE_APP_URL— your deployed app URL (e.g.https://your-app.vercel.app)- Optional
LORE_APP_URL_LOCAL— only if you need a non-default local callback port - Optional
NEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY— geographic cache (see below) - Optional
NEXT_PUBLIC_GA_MEASUREMENT_ID— Google Analytics 4 (e.g.G-XXXXXXXXXX)
- Create a Supabase project and enable the PostGIS extension (Database → Extensions).
- Run the SQL migrations in
supabase/migrations/in order in the SQL editor (001_lore_cards.sqlfor cached cards;002_lore_searches.sqlfor community search counts;003_lore_gemini_logs.sqlfor Gemini failure/fallback logs). - Add
NEXT_PUBLIC_SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY(service role, server-only) to.env.localand Vercel. - After a lore search completes, rows appear in
lore_cards. Pan the map to load cached pins viaGET /api/lore/cached?west=&south=&east=&north=. Whengemini-3.1-flash-litefails or a fallback runs, rows are appended tolore_gemini_logs(query in the Supabase SQL editor or Table Editor).
Re-searching the same Wikipedia article updates the existing row (page_id is unique), it does not duplicate.
-
QStash local dev: In a second terminal, run the QStash CLI:
npx @upstash/qstash-cli dev
Copy its
QSTASH_URL,QSTASH_TOKEN, and signing keys into.env.local. WhenQSTASH_URLpoints at127.0.0.1:8080, the app automatically callbacks tohttp://127.0.0.1:3000duringnpm run dev(even ifLORE_APP_URLis your Vercel URL). View messages at the local QStash console, not the cloud dashboard. -
In the Mapbox dashboard, restrict your token URLs to:
http://localhost:3000/*
-
Start the dev server:
npm run dev
-
Open http://localhost:3000 and search for a location.
- Push this repository to GitHub.
- Import the project in Vercel.
- Add environment variables from
.env.example(including QStash, Redis, andLORE_APP_URLset tohttps://your-app.vercel.appor rely onVERCEL_URL). - Deploy. After the first deploy, add your production URL to Mapbox token URL restrictions.
- Verify search → map → lore pins after the queue processes.
No extra vercel.json configuration is required; Vercel auto-detects Next.js.
| Command | Description |
|---|---|
npm run dev |
Start development server |
npm run build |
Production build |
npm run start |
Run production server |
npm run lint |
Run ESLint |
src/
app/
api/lore/ # Enqueue + poll lore jobs
api/lore/cached/ # Bbox query for cached lore cards
api/cron/process-lore/ # QStash worker (Gemini synthesis + Supabase upsert)
components/ # UI components
hooks/ # Shared React hooks
lib/
jobs/ # Redis job store
lore/ # Lore agent schema, synthesis, Supabase cache
supabase/ # Supabase server client
mapbox/ # Mapbox GL token, cached lore layers
supabase/migrations/ # PostGIS schema for lore_cards
photon/ # Photon geocoding API client
qstash/ # QStash publish client
types/ # Shared TypeScript types
wikipedia/ # Wikipedia nearcoord search + article enrichment
- Geographic search cache (skip Gemini when an area already has enough cached cards)
- Supabase Realtime instead of polling (optional)