LocaleScope
LocaleScope is a React + Vite app for researching local markets, saving business results in Postgres, and reviewing them in dashboard and map views.
Stack
- React 19 + Vite
- Local Fastify API + pg-boss worker
- PostgreSQL + PostGIS
- Google Maps Platform for browser maps and Places search
Local App Setup
- Install dependencies:
npm install - Copy
.env.exampleto.env.localand fill in at least:VITE_GOOGLE_MAPS_PLATFORM_KEYDATABASE_URLCOOKIE_SECRETGOOGLE_MAPS_SERVER_KEY- Stripe env vars below if you want to test billing locally
- Run the user frontend:
npm run dev:web - Run the admin frontend (dedicated surface):
npm run dev:admin
The local backend and scripts load both .env and .env.local, with .env.local taking precedence, so you can keep the full local setup in one file during development.
If you open the app from another machine on your LAN, set VITE_API_BASE_URL and APP_ORIGIN to that host instead of localhost. In development, the frontend also auto-rewrites a localhost API URL to the current browser hostname to make local-network testing easier.
Local API Setup
- Ensure PostgreSQL is running locally with PostGIS available.
- Apply the local database migrations:
npm run migrate - Start the API:
npm run dev:api - Start the worker:
npm run dev:worker
First Run Checklist
- Install dependencies:
npm install - Copy
.env.exampleto.env.localand fill required keys:DATABASE_URL,COOKIE_SECRET,GOOGLE_MAPS_SERVER_KEY,VITE_GOOGLE_MAPS_PLATFORM_KEY, and Stripe keys if testing billing - Run migrations:
npm run migrate - Set
ALLOW_ADMIN_BOOTSTRAP=trueand defineADMIN_BOOTSTRAP_TOKEN - Start app web, admin web, API, and worker:
npm run dev:web,npm run dev:admin,npm run dev:api,npm run dev:worker - Visit the admin surface and create the first site admin
- Disable bootstrap after first admin creation:
ALLOW_ADMIN_BOOTSTRAP=false - Verify admin billing access at
/api/admin/billing/workspaces
First-run Admin Bootstrap
Bootstrap mode is only needed when no active application admin exists.
- The DB-backed
application_adminstable is the primary source of truth for app-admin access. ADMIN_EMAILSandBILLING_ADMIN_EMAILSare fallback allowlists during rollout.
- Run migrations first:
npm run migrate - Set
ALLOW_ADMIN_BOOTSTRAP=trueandADMIN_BOOTSTRAP_TOKENin your env file - Visit the dedicated admin surface, then create the first account in "Create first site admin" mode
- After the first admin is created, set
ALLOW_ADMIN_BOOTSTRAP=false
Stripe Billing Setup
Stripe is now the active payments integration for self-serve subscriptions and one-time export packs.
Configure these server-side env vars to enable billing routes:
STRIPE_PUBLISHABLE_KEYSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_PRICE_STARTER_MONTHLYSTRIPE_PRICE_STARTER_ANNUALSTRIPE_PRICE_GROWTH_MONTHLYSTRIPE_PRICE_GROWTH_ANNUALSTRIPE_PRICE_PRO_MONTHLYSTRIPE_PRICE_PRO_ANNUALSTRIPE_PRICE_EXPORT_PACK_10KSTRIPE_PRICE_EXPORT_PACK_50KSTRIPE_BILLING_PORTAL_CONFIGURATION_IDoptionalADMIN_EMAILSoptional comma-separated allowlist for internal app-admin access (preferred)BILLING_ADMIN_EMAILSoptional deprecated fallback allowlist used whenADMIN_EMAILSis unset
Notes:
- The internal catalog in
shared/billing/plans.tsandshared/billing/addons.tsremains canonical. Stripe price IDs are environment-specific mappings only. - Apply migrations before testing Stripe webhooks so
billing_webhook_eventsexists:npm run migrate - Enterprise stays on a manual sales/invoicing path and does not use self-serve checkout.
- Stripe lifecycle hardening now treats
past_duesubscriptions as a grace-window state before chargeable actions hard-block. The default grace window is 7 days. - Billing return notices now appear on the account page for completed and canceled checkout flows.
- Internal billing support visibility is available through
/api/admin/billing/workspacesfor allowlisted admin emails.
Admin Operations Runbook
Bootstrap/security hardening checklist after first-run admin setup:
- Set
ALLOW_ADMIN_BOOTSTRAP=falseand redeploy API. - Rotate
ADMIN_BOOTSTRAP_TOKENand store it in your secrets manager. - Ensure at least two active app-admin identities are configured.
- Prefer
ADMIN_EMAILS; stop relying on deprecatedBILLING_ADMIN_EMAILSfallback.
Support diagnostics starting thresholds:
- Failed webhooks: investigate when there are 5+ failures in 15 minutes or 20+ failures in 24 hours.
- Stale sync accounts: investigate when 10+ workspaces are stale for more than 24 hours.
- Repeated payment failures: investigate any workspace with 3+
invoice_payment_failedevents in 7 days. - Pending plan effective in past: investigate when count remains above 0 for more than 2 hours.
First-response sequence for repeated failures:
- Open Admin Console diagnostics and capture affected workspace IDs.
- Open each workspace detail and review recent timeline and webhook event history.
- Verify Stripe webhook delivery status and replay failed events where safe.
- Confirm billing sync recovers and anomaly counts return toward baseline.
- Escalate with captured event IDs and workspace IDs if issues persist.
Safe mutation pilot:
- Admin Console now includes a constrained billing resync mutation.
- Required inputs:
workspaceId, operational reason, and typed confirmation (RESYNC). - Optional input:
ticketReffor support/incident traceability.
Docker Deployment
- Copy
.env.exampleto.envand set at least:DATABASE_URLto the Compose database host, for examplepostgres://postgres:YOUR_PASSWORD@db:5432/leads4lessPOSTGRES_DBPOSTGRES_USERPOSTGRES_PASSWORDWEB_PORTADMIN_WEB_PORTAPP_PORTCOOKIE_SECRETGOOGLE_MAPS_SERVER_KEYVITE_API_BASE_URL(use/apiwhen web/admin and API share a domain via the nginx proxy)VITE_GOOGLE_MAPS_PLATFORM_KEY
- Create the shared Docker network if it does not exist:
docker network create locale-all || true - Build and start the full stack:
docker compose up --build
The Compose stack starts PostGIS, waits for the database to become healthy, runs migrations automatically, and then starts the API, worker, user-web, and admin-web containers.
With the default .env.example values, the user app is exposed on http://localhost:3000, the admin app on http://localhost:3001, and the API on http://localhost:4000.
App and Admin Surfaces
- User and admin UIs now run on separate frontend surfaces/build targets.
- The main app no longer links to or renders admin routes/tabs.
- Admin tasks live on the dedicated admin surface and continue using the same API/session auth.
APP_ORIGINshould include both origins for CORS/session checks, for example:https://localescope.com,https://admin.localescope.com.
Database Layout
db/migrations/0001_local_core.sqlcreates the local-first schema.db/scripts/migrate.tsapplies migrations in order and records them inschema_migrations.db/scripts/import-postal-areas.tsimports US ZIP/ZCTA and Canada FSA polygons from local GeoJSON files.db/scripts/build-postal-neighbors.tsbuilds adjacency links for deep research propagation.db/datasets/README.mddocuments the expected postal dataset locations and property names.
Postal Dataset Setup
- Place your GeoJSON files in:
db/datasets/postal/us_zcta.geojsondb/datasets/postal/ca_fsa.geojson
- Or override them with:
POSTAL_US_DATASET_PATHPOSTAL_CA_DATASET_PATH
- Import and build adjacency:
npm run seed:postal
Notes:
- Raw postal source files and generated GeoJSON datasets under
db/rawdata/anddb/datasets/postal/*.geojsonare ignored by git. - The importer expects WGS84 lon/lat GeoJSON. If a source file is projected, re-export it to
EPSG:4326before importing.
Observability commands:
npm run import:postallogs progress every 500 features by default.npm run build:postal-neighborslogs per-country progress and final adjacency totals.npm run check:postalprints current postal area and neighbor counts from the database.
You can change the import log interval with POSTAL_IMPORT_LOG_EVERY, for example:
POSTAL_IMPORT_LOG_EVERY=1000 npm run import:postal
If you want to validate the current postal tables after an import or neighbor build, run:
npm run check:postal
Google Maps Requirements
Enable these Google Cloud APIs for the keys you use:
- Maps JavaScript API
- Places API
- Geocoding API
Use a browser-restricted key for VITE_GOOGLE_MAPS_PLATFORM_KEY and a server-side key for GOOGLE_MAPS_SERVER_KEY.
Current Runtime Flow
- The React app authenticates through the local Fastify API using cookie-backed sessions.
- Search requests run through the local API and persist results in PostgreSQL.
- The worker foundation is available for asynchronous job execution and deep research expansion.
- The target architecture is fully local auth + Fastify routes + pg-boss workers + PostgreSQL/PostGIS.