Integration — External widget → Backend
The Svelte 5 widget never holds a JWT. Authentication against the backend is done entirely via the finder token embedded in the URL path or as a query parameter. This page covers every backend surface the widget calls at runtime, how each call is authenticated, rate-limiting constraints, how the backend resolves a finder from a key, caching behavior, and what the widget does when the backend is unreachable.
All widget network code lives in a single module: dropafinder-app-external/ext/2/source/src/lib/api.js.
For the broader runtime flow these calls fit into, see Data flow — end user. For CDN file layout and cache TTLs, see Storage and CDN. For how the widget initializes before any backend call is made, see External widget — init flow.
Authentication model
The widget has no concept of a logged-in user. It authenticates by presenting the finder token — a short opaque string — as part of each request. There is no Authorization header, no session cookie, and no JWT.
How the token reaches the backend varies by endpoint:
GET /api/ext/finders/details/{key}— token is a URL path segment (the{key}route parameter). Source:api.js:82.GET /api/mapping/autocompleteandGET /api/mapping/places— token is a query parameter (finder_token). Source:api.js:116,api.js:143.POST /api/analytics— token is a JSON body field (finderId). Source:AnalyticsController.php:17.
The backend never issues a challenge or returns a bearer token to the widget. The finder token is effectively a public capability token that grants read access to the specific finder’s data.
The four public endpoints the widget calls
1. GET /api/ext/finders/details/{key} — origin fallback for finder config
Route: routes/api.php:218 — no middleware beyond Laravel defaults (no auth:api, no throttle).
Purpose: Returns the full finder payload (config, settings, locations) from the origin. This is the final fallback in the three-level CDN read chain. In normal production operation the widget resolves via the CDN fast path and does not hit this endpoint. It is always called in development (before CDN infrastructure is live) and on CDN failure.
Widget call site: api.js:82
const response = await fetch(`${API_BASE}/ext/finders/details/${finderKey}`);API_BASE is import.meta.env.VITE_API_APP_URL — a build-time environment variable, e.g. https://api.locationfinders.com/api.
Finder key resolution: The controller (ExternalController::getFinderDetails) looks up the finder with:
$finder = Finder::with('maps.sets.locations', 'maps.locations')
->where('token', $key)
->first();(ExternalController.php:95–97)
If no finder matches, it returns {"message": "Finder not found"} with HTTP 403.
Domain authorization check: Before returning data, the controller checks the Origin (or Referer) request header against the finder’s authorized_urls list (ExternalController.php:93–121). Matching is hostname-exact — parse_url(..., PHP_URL_HOST) is compared on both sides. Two special cases apply:
- If
authorized_urlsis empty, all origins are allowed. localhostand127.0.0.1are always allowed regardless of the list.
An unauthorized origin returns {"error": "Unauthorized domain"} with HTTP 403.
Caching inside the controller: After authorization, the controller uses Laravel’s Cache facade with the key finder_details_{token} (ExternalController.php:123–129). If the cache is cold, buildPayload() assembles the response and stores it. No explicit TTL is set, so the cache uses the default driver TTL (application-configured). This cache is separate from the CDN layer — it is a server-side process cache, not a CDN edge cache.
Response shape:
{
"finder": {
"token": "abc123",
"settings": [...],
"design": {...},
"authorized_urls": [...],
"advanced_refinements": {...},
"custom_field_definitions": [...]
},
"locations": [
{
"id": 1,
"title": "...",
"address": { "street_address_1": "...", "city": "...", "state": "...", ... },
"latitude": 37.7749,
"longitude": -122.4194,
"phone": null,
"website": null,
"image": "...",
"image_url": "...",
"tags": [...],
"custom_fields": [...]
}
],
"created": "2026-04-27T12:00:00Z"
}The CDN-published version of this payload also includes hash and published_at fields (added by syncFinderDetails before the R2 write). The origin fallback response does not include those fields. See Data flow — end user § Phase 4 for how the widget handles the two shapes.
⚠️ Warning: One setting receives special handling at response time. If
design.map.tileSetis"tomtom", the controller strips any customer-supplieddesign.map.apiKeyentry from the settings array and injects the server-side TomTom key fromconfig('services.tomtom.api_key')in its place (ExternalController.php:132–138). This means the TomTom API key is never sent from the widget; it is injected server-side on each request.
2. GET /api/mapping/autocomplete — address autocomplete proxy
Route: routes/api.php:212 — wrapped in Route::middleware('throttle:30,1').
Purpose: Proxies address autocomplete requests to the per-finder autocomplete provider. The backend hides all third-party API keys; the widget never sees them.
Widget call site: api.js:97–133 (fetchAutocompleteSuggestions)
Query parameters sent by the widget:
| Parameter | Required | Source |
|---|---|---|
input | Yes | User-typed string |
sessionToken | No | UUID generated per search session (groups billing on Google Places) |
latitude | No | Viewer’s resolved location (IP or GPS) |
longitude | No | Viewer’s resolved location (IP or GPS) |
finder_token | No | Finder key (passed so backend can resolve provider) |
Finder key use: The finder_token query param is not a hard auth check — it is used only to resolve which autocomplete provider (and which API key) the finder is configured to use. The controller caches [providerSlug, finderId] under the key autocomplete_finder_{token} for 60 seconds (MappingController.php:76–89). The decrypted third-party API key is fetched fresh on every request and never stored in cache (MappingController.php:96–98).
If finder_token is absent or no matching finder exists, the controller falls back to the dropaSearch provider (MappingController.php:70–73, 91–93).
Response shape:
{ "predictions": [ ... ] }On error (network or validation failure) the widget catches and returns an empty array — the search bar degrades silently (api.js:122–126, Search.svelte:208–209).
3. GET /api/mapping/places — place details by ID
Route: routes/api.php:213 — same throttle:30,1 middleware group as autocomplete.
Purpose: Resolves a provider-specific place ID to coordinates and address detail. Called after the user selects an autocomplete suggestion.
Widget call site: api.js:135–160 (fetchPlaceDetails)
Query parameters sent:
| Parameter | Required | Source |
|---|---|---|
place_id | Yes | Place ID returned by the autocomplete prediction |
sessionToken | No | Same session token used for the matching autocomplete call |
finder_token | No | Same finder key; same provider resolution logic as autocomplete |
Response shape:
{ "result": { ... } }result may be null — the comment in MappingController.php:65 notes this signals the widget to fall back to inline coordinates from the autocomplete prediction. On a non-OK HTTP status, fetchPlaceDetails throws an Error with the message from errorData.error || errorData.message (api.js:149–155). The Search.svelte caller catches and swallows the error, clearing the selection silently (Search.svelte:388).
4. POST /api/analytics — visitor event ingestion
Route: routes/api.php:210 — no throttle middleware, no auth:api.
Purpose: Receives batched visitor interaction events (loads, searches, clicks, filter applications) for storage in the analytics table.
This endpoint is not in the ext/ prefix group but is equally public — the widget calls it unauthenticated. The finderId body field performs implicit finder resolution via the exists:finders,token validation rule (AnalyticsController.php:17).
Widget call site: src/lib/analytics.js (called from App.svelte and interaction handlers).
For a full description of the analytics event schema, see the Analytics embedding guide.
Rate limiting
The throttle middleware applied to the mapping endpoints is declared in routes/api.php:211:
Route::middleware('throttle:30,1')->group(function () {
Route::get('/mapping/autocomplete', ...);
Route::get('/mapping/places', ...);
});throttle:30,1 is Laravel’s built-in ThrottleRequests middleware — 30 requests per 1 minute, keyed by IP address. This is a global limit across all finders and all users sharing an IP; it is not per-finder or per-tenant.
💡 Tip: The 30 req/min limit applies per visitor IP, not per finder. A user who types quickly in the search bar (autocomplete debounces at 250 ms in
Search.svelte:224) will typically produce 1–3 requests per search interaction. Rate limit exhaustion is unlikely for normal usage but could be reached by automated scanning.
When the limit is exceeded the backend returns HTTP 429. fetchAutocompleteSuggestions catches any network-level error and returns []; it does not explicitly catch 429 — a non-OK status causes the function to also return [] (api.js:127–128). The widget search bar therefore shows no suggestions without surfacing an error to the user.
The GET /api/ext/finders/details/{key} origin fallback and POST /api/analytics have no throttle middleware applied.
CDN caching behavior and the origin fallback relationship
For production payloads, the widget does not call GET /api/ext/finders/details/{key} under normal conditions. It uses a two-file CDN pattern:
- Pointer file —
https://cdn.locationfinders.com/finders/{key}/config.json— CDN TTL 60 seconds (max-age=60). Contains{ hash, published_at }. - Versioned payload file —
https://cdn.locationfinders.com/finders/{key}/v/{hash}.json— CDN TTL 1 year (max-age=31536000, immutable). Contains the full finder payload.
These files are written by ExternalController::syncFinderDetails when the dashboard operator publishes a finder. The widget also checks localStorage (key lf-cdn-v2:{token}) before hitting the CDN, so a repeat visit with no intervening publish skips all network calls for the payload.
The origin API endpoint (/api/ext/finders/details/{key}) is the fallback of last resort: called when both the pointer fetch and the versioned payload fetch fail, and always in development.
For the full three-level read sequence with a decision diagram, see Data flow — end user § Phase 4. For CDN file layout and TTL rationale, see Storage and CDN.
Error handling when the backend is unreachable
Each function in api.js handles backend unavailability differently:
fetchFinderPointer — silent null return
try {
const res = await fetch(`${CDN_BASE}/finders/${finderKey}/config.json`);
if (!res.ok) return null;
...
} catch {
return null;
}Any fetch error or non-OK response returns null. The caller (fetchFinderPayload) treats a null pointer as a signal to skip straight to the origin API fallback. (api.js:40–50)
fetchFinderPayload — throws on total failure
The function works through the three levels silently (CDN + localStorage errors are caught internally) but throws if the final origin API call fails:
const response = await fetch(`${API_BASE}/ext/finders/details/${finderKey}`);
if (!response.ok) {
...
throw new Error(message);
}(api.js:82–94)
The caller in App.svelte::boot() catches the thrown error and writes its message to the error reactive variable (App.svelte:247–248):
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load finder.';
} finally {
loading = false;
}When error is non-empty, App.svelte:375 renders a .finderRuntimePlaceholder.errorState container with a .messageCard.error card displaying the error string. The widget never renders its interactive UI — the host page sees a placeholder with an error message. (App.svelte:375–424)
fetchAutocompleteSuggestions — silent empty array
Network errors return [] via a top-level try/catch (api.js:122–126). Non-OK responses also return [] (api.js:127–128). The search bar shows no dropdown without any error state.
fetchPlaceDetails — throws, caught by Search.svelte
Non-OK responses throw an Error. Search.svelte:388 catches the error and swallows it — no visible feedback to the visitor.
fetchIpLocation — graceful geolocation degradation
If IP geolocation fails, App.svelte calls setUserLocation with { lat: null, lng: null, error: 'Unable to determine viewer location.' } (App.svelte:151–156). The widget renders normally; distance-based sorting and map centering are disabled.
Summary table
| Endpoint | Auth | Throttle | On error |
|---|---|---|---|
GET /ext/finders/details/{key} | Token in URL path; domain check via Origin/Referer header | None | Widget throws → renders error card |
GET /mapping/autocomplete | finder_token query param (provider resolution only) | 30 req/min per IP (throttle:30,1) | Returns []; silent |
GET /mapping/places | finder_token query param (provider resolution only) | 30 req/min per IP (throttle:30,1) | Throws; caught silently by Search.svelte |
POST /analytics | finderId body field (token validated via exists:finders,token) | None | 🔴 [NEEDS CLARIFICATION: does the widget retry failed analytics events or discard them?] |
Related documents
- Integration points — index — the four system boundaries at a glance
- Data flow — end user — full seven-phase runtime trace including the CDN fast path
- Storage and CDN — R2 layout, CDN TTLs, payload generation trigger
- External widget — init flow — how the widget bootstraps before any backend call
- Embedding — caching — customer-facing explanation of CDN caching behavior
- Snippet loader — how the initial script tag resolves to the widget bundle