These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

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

ServiceRepoStackHosted onURL
Dashboarddropafinder-app-nextjsNext.js 16, TypeScriptVercel(frontend domain — clarify)
APIdropafinder-app-backendLaravel 11, PHP 8.2Cloudwaysapi.locationfinders.com
Widgetdropafinder-app-externalSvelte 5, MapLibreCloudflare R2 + CDNcdn.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:

  1. 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.
  2. The pointer/payload split. The widget fetches a small pointer (5-min TTL) that names a hash-versioned payload (immutable per hash). This lets us cache payloads forever and only invalidate the small pointer on edits. See decisions/cdn-pointer-pattern.
  3. Public token, no JWT. Visitor-side fetches use the finder’s public token in 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:

  1. 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.
  2. Workspace scoping is a query param. When a workspace is active in workspaceStore, every fetchAPI call auto-appends workspace_id. Server-side, the same scoping is enforced.
  3. 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_PAYLOAD on window). 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:

FromToHow
Next.js → BackendJWT-authenticated RESTfetchAPI / fetchExt wrappers; auth header + audit headers; workspace scoping. Detail.
Widget → BackendPublic + token-keyed RESTToken in URL path; throttled mapping/* endpoints. Detail.
Next.js → WidgetSnippet route + preview-mode bridgeapi/ext/v2/snippet/route.ts serves the widget JS in dev/prod; Live Preview uses the preview-mode globals. Detail.
Backend → 3rd-partyOutbound HTTPPaddle, 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

DataLives inWhy
User account, workspace, billing stateBackend DBSingle source of truth for auth and tier gating.
Finder design + locations (canonical)Backend DBEdited by Dashboard; consumed by Widget.
Finder design + locations (cached)Cloudflare R2Visitor-facing; read by Widget.
JWTBrowser (Dashboard)Bearer token in authStore (Zustand).
Visitor session stateBrowser (Widget)Filtering / map state in Svelte stores; not persisted past page unload except payload cache in localStorage.
Analytics eventsBackend DB (analytics table)Aggregated reads via GET /finders/{id}/analytics.
Audit logBackend DB (audit_logs table)Append-only; no retention job today.

Auth model summary

  • Dashboard auth: JWT issued by POST /login, stored in authStore. Custom claims include role, subscription_status, subscription_tier. Validated by auth:api middleware on every protected route.
  • Widget auth: Public finder token in URL path. No JWT. Throttling on the mapping/* endpoints stands in for per-tenant rate limits.
  • Admin gating: role:admin middleware on /admin/* routes.
  • Tier gating: subscription_tier:premium middleware on /workspaces/*.

Detail: auth, backend/auth-and-roles.

Storage and CDN

  • Backend uses Flysystem with the s3 driver. Configured via FILESYSTEM_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) and ext/2 (Svelte 5 + MapLibre, active). The customer-pasted snippet URL points at the root loader, which currently routes to ext/2. See decisions/two-widget-versions.
  • The widget runtime supports a variant mechanism (pointer.variant → dynamic import of ext/v2/variants/{variant}.js). Not currently used in production but available for A/B and progressive rollout.

Deploys

ServiceCommandWhere
Backendmake deployCloudways via SSH
Widgetnpm run deploy:02Cloudflare R2 + cache purge
Dashboardgit push to mainVercel 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

Where to next