Architecture overview
DropAFinder is three deployable services that talk to each other and to two external systems (Paddle for billing, Cloudflare R2 for CDN). This page is the map. Read it before diving into individual codebases.
The three services
| Service | Repo | Stack | Hosted on | URL |
|---|---|---|---|---|
| Dashboard | dropafinder-app-nextjs | Next.js 16, TypeScript | Vercel | (frontend domain — clarify) |
| API | dropafinder-app-backend | Laravel 11, PHP 8.2 | Cloudways | api.locationfinders.com |
| Widget | dropafinder-app-external | Svelte 5, MapLibre | Cloudflare R2 + CDN | cdn.locationfinders.com |
Each service has its own deploy ritual, its own env vars, and its own audience:
- The Dashboard is for workspace owners. Behind JWT auth.
- The API serves both the Dashboard (authed) and the Widget (token-keyed, public).
- The Widget is what your customers’ visitors load and interact with.
🔴 [NEEDS CLARIFICATION: Production frontend domain. Backend uses
locationfinders.com; the brand split between LocationFinders and DropAFinder is not obvious from the digest.]
Data flow — the visitor path
This is the higher-volume path. A visitor lands on a customer’s website that has the widget embedded; the widget reaches DropAFinder for finder data and renders.
Three notable choices in this flow:
- The widget loads from CDN, not from the API. This is for visitor-side latency and edge cache hit rate. The CDN is the primary path; the API is fallback.
- The pointer/payload split. The widget fetches a small
pointer(5-min TTL) that names a hash-versionedpayload(immutable per hash). This lets us cache payloads forever and only invalidate the small pointer on edits. See decisions/cdn-pointer-pattern. - Public token, no JWT. Visitor-side fetches use the finder’s public
tokenin the URL path. The token grants read of one finder’s design + locations and write of analytics events. No secrets exist on the visitor side.
Detail: data-flow-end-user.
Data flow — the builder path
This is the lower-volume but higher-fidelity path. A workspace owner edits a finder in the Dashboard; changes persist to the backend and propagate to the CDN.
Three notable choices here:
- Audit headers are client-set. The Dashboard names the action (
X-Audit-Action), resource (X-Audit-Resource-Type/-Id), and request id; the backend records what it’s told plus the request payload + status. See decisions/audit-headers-on-mutations. - Workspace scoping is a query param. When a workspace is active in
workspaceStore, everyfetchAPIcall auto-appendsworkspace_id. Server-side, the same scoping is enforced. - Preview is in-process. The Live Preview embeds the same widget runtime inside the Dashboard via the preview-mode bridge (
__LF_PREVIEW_MODE,__LF_PREVIEW_SESSION,__LF_PREVIEW_PAYLOADonwindow). Unsaved changes flow to the runtime via the payload global, bypassing the CDN. See external/preview-bridge.
Detail: data-flow-builder.
How the services know about each other
There are four integration boundaries. Each has a dedicated reference page:
| From | To | How |
|---|---|---|
| Next.js → Backend | JWT-authenticated REST | fetchAPI / fetchExt wrappers; auth header + audit headers; workspace scoping. Detail. |
| Widget → Backend | Public + token-keyed REST | Token in URL path; throttled mapping/* endpoints. Detail. |
| Next.js → Widget | Snippet route + preview-mode bridge | api/ext/v2/snippet/route.ts serves the widget JS in dev/prod; Live Preview uses the preview-mode globals. Detail. |
| Backend → 3rd-party | Outbound HTTP | Paddle, S3/R2, six autocomplete providers. Detail. |
There is no Backend → Next.js direction. The backend never initiates a request to the dashboard.
There is no direct Widget → Next.js communication in production. The widget never calls the dashboard; the dashboard reaches the widget only via the preview-mode bridge during Live Preview.
State and data ownership
| Data | Lives in | Why |
|---|---|---|
| User account, workspace, billing state | Backend DB | Single source of truth for auth and tier gating. |
| Finder design + locations (canonical) | Backend DB | Edited by Dashboard; consumed by Widget. |
| Finder design + locations (cached) | Cloudflare R2 | Visitor-facing; read by Widget. |
| JWT | Browser (Dashboard) | Bearer token in authStore (Zustand). |
| Visitor session state | Browser (Widget) | Filtering / map state in Svelte stores; not persisted past page unload except payload cache in localStorage. |
| Analytics events | Backend DB (analytics table) | Aggregated reads via GET /finders/{id}/analytics. |
| Audit log | Backend DB (audit_logs table) | Append-only; no retention job today. |
Auth model summary
- Dashboard auth: JWT issued by
POST /login, stored inauthStore. Custom claims includerole,subscription_status,subscription_tier. Validated byauth:apimiddleware on every protected route. - Widget auth: Public finder
tokenin URL path. No JWT. Throttling on themapping/*endpoints stands in for per-tenant rate limits. - Admin gating:
role:adminmiddleware on/admin/*routes. - Tier gating:
subscription_tier:premiummiddleware on/workspaces/*.
Detail: auth, backend/auth-and-roles.
Storage and CDN
- Backend uses Flysystem with the
s3driver. Configured viaFILESYSTEM_DISK, can target AWS S3 or Cloudflare R2. - Location images: uploaded via
POST /locations/:id/image, stored in S3/R2, returned as a CDN URL. - Finder payloads: written to R2 by the backend on Finder save, fetched by the widget through the CDN.
- Widget snippet artifacts: built by the external repo’s
npm run deploy:02, uploaded to R2, Cloudflare cache purged.
Detail: storage-and-cdn.
Versioning posture
- The API is unversioned. There is no
/v1/,/v2/prefix. Breaking changes need to be coordinated across the Dashboard and Widget. - The Dashboard has internal v2 / v3 / legacy variants of the Finder Builder. v2 is production; v3 is a PoC; legacy is still in the repo. See decisions/two-finder-builder-versions.
- The Widget has two builds:
ext/1(Svelte 4 + Leaflet, legacy) andext/2(Svelte 5 + MapLibre, active). The customer-pasted snippet URL points at the root loader, which currently routes toext/2. See decisions/two-widget-versions. - The widget runtime supports a variant mechanism (
pointer.variant→ dynamic import ofext/v2/variants/{variant}.js). Not currently used in production but available for A/B and progressive rollout.
Deploys
| Service | Command | Where |
|---|---|---|
| Backend | make deploy | Cloudways via SSH |
| Widget | npm run deploy:02 | Cloudflare R2 + cache purge |
| Dashboard | git push to main | Vercel auto-deploy |
There is no orchestration that deploys all three in concert. For coordinated changes, the recommended order is backend (additive migration) → widget (new payload-shape consumer) → dashboard (new payload-shape producer) so the read side accepts the new shape before the write side sends it.
Detail: deployments, how-to/deploy.
What this overview does not cover
- Per-codebase file structure — see codebases/.
- Endpoint catalog — see reference/api/.
- Data model field-level reference — see reference/data-models/.
- Step-by-step “how do I do X” guides — see how-to/.
Where to next
- Bring everything up locally: how-to/run-locally
- Add a feature end-to-end: how-to/add-api-endpoint-end-to-end
- Per-service deep dive: pick from codebases/