203 lines
9.1 KiB
Markdown
203 lines
9.1 KiB
Markdown
# 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
|
|
|
|
1. Install dependencies:
|
|
`npm install`
|
|
2. Copy `.env.example` to `.env.local` and fill in at least:
|
|
- `VITE_GOOGLE_MAPS_PLATFORM_KEY`
|
|
- `DATABASE_URL`
|
|
- `COOKIE_SECRET`
|
|
- `GOOGLE_MAPS_SERVER_KEY`
|
|
- Stripe env vars below if you want to test billing locally
|
|
3. Run the user frontend:
|
|
`npm run dev:web`
|
|
4. 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
|
|
|
|
1. Ensure PostgreSQL is running locally with PostGIS available.
|
|
2. Apply the local database migrations:
|
|
`npm run migrate`
|
|
3. Start the API:
|
|
`npm run dev:api`
|
|
4. Start the worker:
|
|
`npm run dev:worker`
|
|
|
|
## First Run Checklist
|
|
|
|
- [ ] Install dependencies: `npm install`
|
|
- [ ] Copy `.env.example` to `.env.local` and 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=true` and define `ADMIN_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_admins` table is the primary source of truth for app-admin access.
|
|
- `ADMIN_EMAILS` and `BILLING_ADMIN_EMAILS` are fallback allowlists during rollout.
|
|
|
|
1. Run migrations first: `npm run migrate`
|
|
2. Set `ALLOW_ADMIN_BOOTSTRAP=true` and `ADMIN_BOOTSTRAP_TOKEN` in your env file
|
|
3. Visit the dedicated admin surface, then create the first account in "Create first site admin" mode
|
|
4. 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_KEY`
|
|
- `STRIPE_SECRET_KEY`
|
|
- `STRIPE_WEBHOOK_SECRET`
|
|
- `STRIPE_PRICE_STARTER_MONTHLY`
|
|
- `STRIPE_PRICE_STARTER_ANNUAL`
|
|
- `STRIPE_PRICE_GROWTH_MONTHLY`
|
|
- `STRIPE_PRICE_GROWTH_ANNUAL`
|
|
- `STRIPE_PRICE_PRO_MONTHLY`
|
|
- `STRIPE_PRICE_PRO_ANNUAL`
|
|
- `STRIPE_PRICE_EXPORT_PACK_10K`
|
|
- `STRIPE_PRICE_EXPORT_PACK_50K`
|
|
- `STRIPE_BILLING_PORTAL_CONFIGURATION_ID` optional
|
|
- `ADMIN_EMAILS` optional comma-separated allowlist for internal app-admin access (preferred)
|
|
- `BILLING_ADMIN_EMAILS` optional deprecated fallback allowlist used when `ADMIN_EMAILS` is unset
|
|
|
|
Notes:
|
|
|
|
- The internal catalog in `shared/billing/plans.ts` and `shared/billing/addons.ts` remains canonical. Stripe price IDs are environment-specific mappings only.
|
|
- Apply migrations before testing Stripe webhooks so `billing_webhook_events` exists: `npm run migrate`
|
|
- Enterprise stays on a manual sales/invoicing path and does not use self-serve checkout.
|
|
- Stripe lifecycle hardening now treats `past_due` subscriptions 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/workspaces` for allowlisted admin emails.
|
|
|
|
## Admin Operations Runbook
|
|
|
|
Bootstrap/security hardening checklist after first-run admin setup:
|
|
|
|
- Set `ALLOW_ADMIN_BOOTSTRAP=false` and redeploy API.
|
|
- Rotate `ADMIN_BOOTSTRAP_TOKEN` and store it in your secrets manager.
|
|
- Ensure at least two active app-admin identities are configured.
|
|
- Prefer `ADMIN_EMAILS`; stop relying on deprecated `BILLING_ADMIN_EMAILS` fallback.
|
|
|
|
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_failed` events 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:
|
|
|
|
1. Open Admin Console diagnostics and capture affected workspace IDs.
|
|
2. Open each workspace detail and review recent timeline and webhook event history.
|
|
3. Verify Stripe webhook delivery status and replay failed events where safe.
|
|
4. Confirm billing sync recovers and anomaly counts return toward baseline.
|
|
5. 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: `ticketRef` for support/incident traceability.
|
|
|
|
## Docker Deployment
|
|
|
|
1. Copy `.env.example` to `.env` and set at least:
|
|
- `DATABASE_URL` to the Compose database host, for example `postgres://postgres:YOUR_PASSWORD@db:5432/leads4less`
|
|
- `POSTGRES_DB`
|
|
- `POSTGRES_USER`
|
|
- `POSTGRES_PASSWORD`
|
|
- `WEB_PORT`
|
|
- `ADMIN_WEB_PORT`
|
|
- `APP_PORT`
|
|
- `COOKIE_SECRET`
|
|
- `GOOGLE_MAPS_SERVER_KEY`
|
|
- `VITE_API_BASE_URL` (use `/api` when web/admin and API share a domain via the nginx proxy)
|
|
- `VITE_GOOGLE_MAPS_PLATFORM_KEY`
|
|
2. Create the shared Docker network if it does not exist:
|
|
`docker network create locale-all || true`
|
|
3. 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_ORIGIN` should include both origins for CORS/session checks, for example: `https://localescope.com,https://admin.localescope.com`.
|
|
|
|
## Database Layout
|
|
|
|
- `db/migrations/0001_local_core.sql` creates the local-first schema.
|
|
- `db/scripts/migrate.ts` applies migrations in order and records them in `schema_migrations`.
|
|
- `db/scripts/import-postal-areas.ts` imports US ZIP/ZCTA and Canada FSA polygons from local GeoJSON files.
|
|
- `db/scripts/build-postal-neighbors.ts` builds adjacency links for deep research propagation.
|
|
- `db/datasets/README.md` documents the expected postal dataset locations and property names.
|
|
|
|
## Postal Dataset Setup
|
|
|
|
1. Place your GeoJSON files in:
|
|
- `db/datasets/postal/us_zcta.geojson`
|
|
- `db/datasets/postal/ca_fsa.geojson`
|
|
2. Or override them with:
|
|
- `POSTAL_US_DATASET_PATH`
|
|
- `POSTAL_CA_DATASET_PATH`
|
|
3. Import and build adjacency:
|
|
`npm run seed:postal`
|
|
|
|
Notes:
|
|
- Raw postal source files and generated GeoJSON datasets under `db/rawdata/` and `db/datasets/postal/*.geojson` are ignored by git.
|
|
- The importer expects WGS84 lon/lat GeoJSON. If a source file is projected, re-export it to `EPSG:4326` before importing.
|
|
|
|
Observability commands:
|
|
- `npm run import:postal` logs progress every 500 features by default.
|
|
- `npm run build:postal-neighbors` logs per-country progress and final adjacency totals.
|
|
- `npm run check:postal` prints 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
|
|
|
|
1. The React app authenticates through the local Fastify API using cookie-backed sessions.
|
|
2. Search requests run through the local API and persist results in PostgreSQL.
|
|
3. The worker foundation is available for asynchronous job execution and deep research expansion.
|
|
4. The target architecture is fully local auth + Fastify routes + pg-boss workers + PostgreSQL/PostGIS.
|