How to — run locally
Bring all three services up on your machine. End state: Dashboard at http://localhost:3000, Backend API at http://127.0.0.1:8000, Widget served from the Dashboard’s /api/ext/v2/snippet route (which reads the local ext/2/snippet.js artifact).
Prerequisites
- PHP 8.2+ with the standard Laravel extensions
- Composer
- Node.js (LTS — Next.js 16 needs Node 20+; widget uses Vite 8)
- npm
- SQLite (default for the backend in dev; no separate DB server needed)
- git with access to the three repos
The three repos must be siblings under /Users/codydavis/Local Sites/:
Local Sites/
├── dropafinder-app-nextjs/
├── dropafinder-app-external/
└── dropafinder-app-backend/The dashboard’s snippet-serving route hardcodes the sibling path ../dropafinder-app-external/ext/2/snippet.{dev.,}js, so the layout matters.
Order of bring-up
Backend → Widget → Dashboard. The dashboard depends on the other two; bringing it up last avoids race-condition-shaped error spam.
Step 1 — Backend
cd "/Users/codydavis/Local Sites/dropafinder-app-backend"
# First time only
composer install
cp .env.example .env
php artisan key:generate
php artisan jwt:secret # if not already set
touch database/database.sqlite
php artisan migrate --seed
# Every time
php artisan serve # serves on http://127.0.0.1:8000In a second terminal, start the queue worker so AI imports + CSV jobs actually run:
php artisan queue:work --queue=aigen,defaultVerify the API is up:
curl http://127.0.0.1:8000/api/users/me
# Expected: 401 Unauthenticated (no token)Backend env vars to fill in
The minimum set to get past php artisan serve:
APP_KEY— generated byphp artisan key:generateDB_CONNECTION=sqlite,DB_DATABASE=database/database.sqliteJWT_SECRET— generated byphp artisan jwt:secret
Optional (features that need them):
MAIL_*— for any flow that sends email (signup confirmations, invites)PADDLE_API_BASE_URLand Paddle webhook secret — for billing flowsR2_*/AWS_*— for image uploadCDN_BASE_URL— for image CDN URLs- AI provider creds — for AI Import / Improvement jobs ([NEEDS CLARIFICATION] on which provider)
Common backend startup gotchas
SQLSTATE[HY000]on first request: you forgotphp artisan migrate. Run it.auth:apireturns 500:JWT_SECRETnot set. Runphp artisan jwt:secret.- CORS error from the dashboard:
config/cors.phpis permissive (*paths, methods, origins) by default; if you tightened it, ensurehttp://localhost:3000is allowed.
Step 2 — Widget
cd "/Users/codydavis/Local Sites/dropafinder-app-external/ext/2/source"
# First time only
npm install
cp .env.development .env.local # if needed; defaults usually fine
# Set VITE_API_APP_URL=http://127.0.0.1:8000/api in .env.local
# Every time
npm run dev # Vite dev server on default portFor most local work, you don’t actually need the widget’s dev server running — the Dashboard’s snippet route reads the built ext/2/snippet.js artifact, not the dev server. So:
# Build the widget once after every code change
npm run build:dev # produces ext/2/snippet.dev.js
# or
npm run build # produces ext/2/snippet.jsThe Dashboard’s snippet route at /api/ext/v2/snippet looks for snippet.dev.js first, falling back to snippet.js, falling back finally to the production CDN.
💡 Tip: If you’re not actively touching widget code, skip
npm installhere and let the dashboard pick up the last-built artifact (or fall through to the production CDN).
Step 3 — Dashboard
cd "/Users/codydavis/Local Sites/dropafinder-app-nextjs"
# First time only
npm install
cp .env.local.example .env.local # if an example exists; otherwise create from scratch
# Edit .env.local — see "Dashboard env vars" below
# Every time
npm run dev # http://localhost:3000Dashboard env vars to fill in
NEXT_PUBLIC_API_URL=http://127.0.0.1:8000/api/
# Map provider keys (any one is enough to start; missing keys = silent fallback)
NEXT_PUBLIC_GOOGLE_KEY=
NEXT_PUBLIC_TOMTOM_KEY=
NEXT_PUBLIC_MAPBOX_KEY=
NEXT_PUBLIC_HERE_KEY=
# Paddle (sandbox values for dev)
NEXT_PUBLIC_PADDLE_ENVIRONMENT=sandbox
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=...
NEXT_PUBLIC_PADDLE_BRONZE_PRODUCT_ID=...
NEXT_PUBLIC_PADDLE_BRONZE_PRICE_ID=...
NEXT_PUBLIC_PADDLE_PREMIUM_PRODUCT_ID=...
NEXT_PUBLIC_PADDLE_PREMIUM_PRICE_ID=...The map provider keys are optional for the dashboard’s own UI but required to render maps in Live Preview; without any, the preview will be empty or fall back to OSM.
Step 4 — End-to-end smoke
- Open
http://localhost:3000 - Register an account
- Create a Location with a real address
- Create a Finder, attach the workspace’s default Map (or create one)
- Open the Finder Builder; the Live Preview rail should render the widget with your one location
- Copy the embed snippet from the Finders list
- Save the snippet HTML to a local file, open in a browser → finder should render
If the Live Preview is empty:
- Check the browser console for fetch errors
- Confirm
npm run build:devproducedext/2/snippet.dev.js - Confirm
NEXT_PUBLIC_API_URLends with a trailing/api/
If the standalone HTML embed is empty:
- The CDN URL
https://cdn.locationfinders.com/snippet.jsis being hit, not your local build. To test against the local build, the embed page would need to loadhttp://localhost:3000/api/ext/v2/snippetinstead. (This is a dev-only configuration, not a customer-facing pattern.)
Useful commands
| Command | Where | Purpose |
|---|---|---|
php artisan migrate:fresh --seed | backend | Wipe and reseed the SQLite DB |
php artisan tinker | backend | REPL with Laravel context |
php artisan queue:listen --queue=aigen | backend | Watch the AI queue with reload |
npm run lint | next.js | ESLint pass |
npm run build | next.js | Production build (occasionally useful for catching SSR issues) |
npm run dev | widget source | Vite dev server (rebuilds on save) |
npm run build:dev | widget source | One-shot dev build → snippet.dev.js |
What is not set up locally
- Cloudflare R2: image uploads in dev use the local
storage/app/publicdisk by default. The widget’s CDN cache pattern doesn’t apply — the Dashboard’s snippet route reads the file directly. - Paddle: a real subscription flow won’t work without sandbox credentials; the Account → Billing page may render but checkout won’t complete.
- Production CDN: the widget pulls finder data from origin (
/ext/finders/details/{key}) in dev, since there’s nothing in R2.
Where to next
- Make a change that touches all three services: add-api-endpoint-end-to-end
- Test what you wrote: test
- Ship to production: deploy