The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the ExplorationMaps listing page.
Browser-based map builder for mineral exploration: search provincial claim registries, import drill/geology data, style investor-ready maps, and export them as PNG/SVG/PDF. Live at explorationmaps.com.
src/), Leaflet map engineapi/)public/
(see scripts/generate-blog.js and scripts/pseo/)package-lock.json is committed — use npm ci for reproducible installs)The serverless functions under api/ run on Vercel. For full-stack local
work use vercel dev; with plain npm run dev the claims search and
analytics endpoints are absent (the UI degrades gracefully).
| Command | What it does |
|---|---|
npm run dev | Vite dev server |
npm run build | Generates the blog, builds to dist/, copies the admin shell |
npm test | Vitest run (CI mode) |
npm run test:watch | Vitest watch mode |
npm run pseo:fixture | pSEO company-pages pipeline against fixture data |
Documented in .env.example. Summary:
Public (VITE_ — compiled into the client bundle, never secret):
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY — Supabase project + anon key
(production-required; without them auth/cloud features disable themselves)VITE_ADMIN_EMAIL — controls admin dashboard visibility only; real
authorization is server-side (admin_users table + is_admin())Server-only secrets (set in Vercel, never referenced by client code):
SUPABASE_SERVICE_ROLE_KEY — used by /api/track for analytics ingestion.
Production-required once migration 20260710000004 is applied.ADMIN_API_SECRET — optional; unlocks the claims-API diagnostic modes in
production via the x-admin-secret header.SUPABASE_URL / SUPABASE_ANON_KEY — optional server-side aliases; the
VITE_-prefixed values are used as fallbacks by api/claims.js (Quebec)
and api/track.js.STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_MONTHLY_ID,
STRIPE_PRICE_YEARLY_ID — Pro-subscription billing (see docs/billing.md).
Until set, billing endpoints return 503 and all plan checks fail open —
safe to deploy the code before configuring Stripe./tenure-monitor)Watches a saved portfolio of B.C. mineral tenures: days remaining against each claim's good-to-date, email reminders, change detection, and one-click hand-off into the map editor. Read-only with respect to government transactions — it never touches MTO on a user's behalf.
MTA_ACQUIRED_TENURE_SVW layer, loaded by
scripts/tenure-sync/ from GitHub Actions (nightly full + twice-daily
targeted). A partial or failed government response aborts without writing,
so a bad day at the province cannot damage stored portfolios.scripts/tenure-alerts/, daily. De-duplication is a unique
database constraint rather than application logic.America/Vancouver throughout
(src/utils/tenureDates.js), never in the browser's timezone.docs/tenure-monitor.md.
User help: docs/tenure-monitor-help.md.api/)api/claims.js — multi-province claims search proxy (BC WFS, ArcGIS
provinces ON/SK/MB/NL/YT, Quebec via a self-hosted Supabase table). Full
pagination with honest meta: { totalKnown, returned, truncated, pagesFetched, provider }; rate-limited; CORS-restricted; sanitized errors.api/bc-claims.js — BC-only WFS proxy (nearby-claims bbox + legacy search).api/tenure-search.js — Tenure Monitor search over the Exploration Maps
mirror of the B.C. mineral tenure registry (tenure number, pasted list, owner
name, client number, map extent, geometry). Anonymous-safe, rate-limited, and
every response states when the mirror was last synchronized. Reads with the
ANON key under the public-read policy, never the service role.api/track.js — all analytics ingestion (page views, live-presence pings,
product/search events, landing clicks, leads). Enforces an event-name
allowlist, payload size/depth limits, and session-id shape; derives geo
from edge headers (never the body); resolves user identity from a verified
Supabase access token; writes with the service role; rate-limits per IP.
Without SUPABASE_SERVICE_ROLE_KEY it accepts-and-drops so analytics can
never break the app.api/stripe-checkout.js / api/stripe-portal.js / api/stripe-webhook.js
— Pro-subscription billing (Stripe Checkout + customer portal + the
signature-verified webhook that syncs public.user_plans). Grandfathered
accounts (pre-launch) are never downgraded. See docs/billing.md.api/_lib/ — shared helpers (pagination, request guards, esri→GeoJSON);
the underscore path means Vercel does not expose them as endpoints.Share links are /map/<id> where <id> is a client-generated
crypto.randomUUID() (a 122-bit random token). Reads go through the
get_shared_map(share_id) RPC — table-level SELECT is revoked, so
shared_maps cannot be enumerated from the public client. Old links keep
working: the id remains the lookup key.
Supabase auth (magic-link default, password fallback). Admin access is a row
in public.admin_users checked by is_admin() inside SECURITY DEFINER
reporting functions; all definer functions have a pinned search_path and
explicit execute grants. To add an admin:
src/utils/projectStorage.js: projects + the working draft live in
localStorage, deflate-compressed (gz1: prefix), with a schema version and
deterministic migrations. Corrupted records are preserved under a
.recovery key (never auto-deleted) and surfaced with manual export/discard
helpers. Writes return structured results — the UI never claims success on a
failed write. On sign-in, local projects migrate to the cloud with
per-project status + retry (src/utils/cloudMigration.js).
CSV (RFC 4180 via Papa Parse; auto-detected lat/long columns, plus azimuth,
dip and length for drill traces), GeoJSON (validated), zipped shapefiles,
loose .shp/.dbf/.prj/.shx sets (the .prj is honored — projected files are
reprojected to WGS84 via proj4), KML, KMZ (bounded: entry count, uncompressed
size, path traversal), and georeferenced images (.png/.jpg/.gif/.webp with
a world file or typed-in edges, lat/long or UTM; resampled to 2048 px and
stored in the project). Published tile ({z}/{x}/{y}) and WMS services can
be added as layers under Reference Overlays.
Canada: BC (WFS), Ontario/Saskatchewan/Manitoba/Newfoundland &
Labrador/Yukon (official ArcGIS services), Quebec (weekly-synced Supabase
mirror). United States: federal BLM MLRS claims for 11 western states,
behind VITE_ENABLE_US_CLAIMS — federal claims only, no state-managed
tenure (Alaska state claims not included); boundaries are generalized BLM
representations, not legal surveys. See docs/us-claims.md for the data
source, field mapping, and the post-deploy verification checklist.
Timestamped, additive migrations live in supabase/migrations/. The legacy
supabase-*.sql files in the repo root are the historical hand-run setup
scripts — they document existing production state; new changes go in
supabase/migrations/ only.
Apply order matters — two migrations are deploy-coupled:
| Step | Action | When |
|---|---|---|
| 1 | 20260710000001_shared_map_lookup_rpc.sql | Any time (additive) |
| 2 | 20260710000003_live_pings_read_lockdown.sql | Any time (the deployed app never reads live_pings) |
| 3 | 20260710000005_admin_authorization.sql | Any time (additive; seeds the current admin) |
| 4 | Deploy the frontend (uses get_shared_map + /api/track) and set SUPABASE_SERVICE_ROLE_KEY in Vercel | — |
| 5 | 20260710000002_shared_maps_lockdown.sql | Only after step 4 |
| 6 | 20260710000004_analytics_ingest_lockdown.sql | Only after step 4 |
Each migration header includes rollback instructions. After applying, verify
RLS as an anonymous client, a normal authenticated user, and the admin —
verification queries are embedded in ...000005.
20260901000001_shared_rate_limit.sql is additive and can be applied any
time, but until it is, the cross-instance rate limiter in api/_lib/guard.js
has nothing to call and fails open (only the per-instance limiter applies).
The send-welcome edge function must also be redeployed
(supabase functions deploy send-welcome) to pick up its lead-row check.
Pushes to main deploy via Vercel (npm run build → dist/, plus api/
functions). Rollback = redeploy the previous Vercel deployment (instant);
if a deploy-coupled migration was applied after the deploy being rolled
back, also run the rollback SQL from that migration's header.
Vitest + React Testing Library (tests/, config in vitest.config.js).
Coverage focuses on regression protection: save/autosave race conditions,
claims request ordering/cancellation, provider pagination, shapefile
projection + CSV/GeoJSON parsing, storage failure/corruption handling,
local→cloud migration retries, API hardening, ArcGIS geometry conversion,
and shared-map access.