CoasterTrak is an MVP rollercoaster tracking app with:
- Interactive map with park/coaster markers
- Email/password auth
- Wishlist tracking
- Ride logging and personal stats
- Install dependencies:
npm install
- Copy env template:
copy .env.example .env.local
- Fill values in
.env.local:NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYSYNC_CRON_SECRETRESEND_API_KEY(friend notification emails)EMAIL_FROM(optional, defaults toCoasterTrak <notify@coastertrak.com>)
- Run schema in Supabase SQL editor:
supabase/schema.sql- Catalog JSON bucket (for hosting
wikidata_coasters.json):supabase/migrations/003_catalog_storage_bucket.sql
- Start app:
npm run dev
- Build a catalog snapshot via CoasterTrak Data (gitignored by default):
npm run data:ingest-wikidatanpm run data:normalize-wikidata -- --latestnpm run data:materialize-snapshot -- --latest
- Optionally enrich / publish locally:
npm run data:enrich-wikipedia -- --enrich-limit 200npm run data:publish-catalog -- --apply(requires service role key)
- Or trigger a full park/coaster upsert from Storage / local JSON:
curl -X POST "http://localhost:3000/api/sync/catalog" -H "Authorization: Bearer <SYNC_CRON_SECRET>"
/- landing page/guides- original coaster credit guides/map- interactive map with country and name filter/login- sign up / sign in/wishlist- user wishlist/stats- personal stats dashboard/api/health- health endpointPOST /api/sync/catalog- protected Wikidata catalog sync (JSON → Supabase).
Manual units render only when both are set:
NEXT_PUBLIC_ADSENSE_ENABLED=trueNEXT_PUBLIC_ADSENSE_GUIDE_SLOT=<slot id from AdSense>
Ads are mounted on guides and selected content pages only — never on thin stubs, login, or app chrome. Keep the flag off until AdSense approves the site.
The "Plan your visit" box on park pages renders only when the flag is on and the park has a confirmed link in src/lib/affiliates.ts. The Privacy Policy and About wording uses the same flag.
NEXT_PUBLIC_AFFILIATES_ENABLED=trueNEXT_PUBLIC_GETYOURGUIDE_PARTNER_ID=<partner id>
These are inlined at build time, so changing them needs a redeploy. Keep partner ids in env, never in code: this repo is public. Keep the flag off until AdSense approves the site.
Vercel Cron hits GET /api/cron/weekly-digest Sundays 18:00 UTC (Authorization: Bearer <SYNC_CRON_SECRET>).
Users get digests by default (notify_weekly_digest). They can uncheck at signup or under Account → Weekly credit digest. Apply migrations 20261006180000_notify_weekly_digest.sql and 20261006183000_weekly_digest_default_on.sql.
- Import repo in Vercel.
- Add env vars from
.env.localin Vercel project settings. - Deploy.
- Validate:
- user signup/signin
- map markers visible
- wishlist and rides saved
- stats totals update
Primary catalog (map pins, coaster rows): built by the CoasterTrak Data pipeline and stored as data/wikidata_coasters.json (working snapshot). The server reads that file from the deployment root, or from WIKIDATA_COASTERS_URL if set (recommended for Vercel, since the JSON is gitignored). Optional override: WIKIDATA_COASTERS_PATH.
Supabase Storage (recommended): apply supabase/migrations/003_catalog_storage_bucket.sql once. Monthly CI gated publish uploads the approved snapshot to the public catalog bucket.
Set WIKIDATA_COASTERS_URL to that URL in Vercel (and locally if you test remote sync).
Optional env overrides: WIKIDATA_STORAGE_BUCKET (default catalog), WIKIDATA_STORAGE_OBJECT (default wikidata_coasters.json), WIKIDATA_COASTERS_ALLOWED_HOSTS (comma-separated host allowlist for WIKIDATA_COASTERS_URL; by default your Supabase project host is allowed).
Avoid checking multi‑MB JSON into git; generate in CI and upload to Storage, then point WIKIDATA_COASTERS_URL at the stable URL.
The GitHub Action .github/workflows/refresh-wikidata.yml runs monthly:
ingest → normalize → materialize → Wikipedia enrich → analyze → validate → gated publish (--apply) → DB Wikipedia backfill
For a full park/coaster upsert from the same Storage URL, Vercel’s weekly cron hits /api/cron/sync-catalog so syncCatalogFromWikidata re-reads WIKIDATA_COASTERS_URL.
Required env vars for server-side sync:
SUPABASE_SERVICE_ROLE_KEYSYNC_CRON_SECRET- For production without committing the JSON:
WIKIDATA_COASTERS_URL
Security notes:
- Sync endpoints (
/api/sync/catalog,/api/cron/sync-catalog) requireAuthorization: Bearer <SYNC_CRON_SECRET>and are rate-limited. - Errors from sync endpoints are intentionally generic; see server logs for details.
Run manually (local dev server, after materializing a snapshot):
curl -X POST http://localhost:3000/api/sync/catalog -H "Authorization: Bearer <SYNC_CRON_SECRET>"
vercel.json schedules /api/cron/sync-catalog weekly (Sundays 05:00 UTC — Wikidata catalog apply).
Stats come from Wikidata (SPARQL → CoasterTrak Data pipeline) and optional Wikipedia enrich/backfill. Empty stats usually mean Wikidata has no quantity, the snapshot was never published, or enrich has not run yet.
Wikipedia infobox DB backfill (optional): for rows that already have wikidata_id but still lack numbers:
npm run wikipedia:backfill— fills null columns only; use--dry-runto preview.
Wikidata exposes RCDB IDs (property P2751). Catalog sync stores them on coasters.rcdb_id and coaster pages may deep-link to rcdb.com.
We do not fetch, scrape, or import RCDB stats or other content. RCDB does not share data with other projects; identifiers come from Wikidata only.
Backfill IDs from the Wikidata snapshot: npm run data:backfill-rcdb-ids after migration 20260904120000_coasters_rcdb_id.sql.
Compare the live catalog against ThemeParks.wiki (existence / naming — not stats):
npm run data:match-themeparks— auto-matches all catalog parks by name + cached DB links; writesdata/themeparks-match-report.jsonnpm run data:match-themeparks -- --write-db— persists links, findings, and review queue- Catalog quality admin:
/admin/data/catalog - Migrations:
20260811122151_*,20260811122641_*,20260811123330_* - Aliases:
data_coaster_name_aliases· field overrides:data_coaster_field_overrides
Lives under src/lib/coastertrak-data/.
data:ingest-wikidata
↓
data:normalize-wikidata
↓
data:materialize-snapshot → data/wikidata_coasters.json
↓
data:enrich-wikipedia
↓
data:analyze-catalog
data:validate-wikidata
↓
data:publish-catalog [--apply]
| Command | Purpose |
|---|---|
data:ingest-wikidata |
Immutable SPARQL bindings → data/raw/wikidata/{runId}/ |
data:normalize-wikidata |
Deduped rows → data/processed/wikidata/{runId}/ |
data:materialize-snapshot |
Copy processed → working data/wikidata_coasters.json |
data:enrich-wikipedia |
Fill gaps from enwiki infobox HTML |
data:backfill-rcdb-ids |
Set coasters.rcdb_id from Wikidata snapshot P2751 (IDs only) |
data:validate-wikidata |
Quality report → data/reports/wikidata/{runId}/ |
data:analyze-catalog |
Dedupe/conflicts + ThemeParks snapshot verify |
data:publish-catalog |
Dry-run by default; --apply uploads to Supabase |
npm test |
Vitest unit tests (also in .github/workflows/ci.yml) |
Gated publish applies known fixes + approved data_coaster_field_overrides before Storage/DB upload. Catalog sync endpoints read the Storage URL that gated publish updates.