@fyremaps/web-sdk
v0.4.3
Published
React / Next.js SDK for FyreMaps — MapLibre GL JS map rendering, geocoding search, and turn-by-turn navigation.
Maintainers
Readme
@fyremaps/web-sdk
@fyremaps/web-sdk is a toolkit for adding maps to a React or Next.js app.
It gives you four building blocks, and you can use as many or as few as
your app needs:
- A map you can show on a page — pan, zoom, add pins, draw routes.
- Search — turn text like
"1600 Amphitheatre Pkwy"into map coordinates (and the other way around). - Directions — get a route between two places, or through several stops in a row.
- Turn-by-turn navigation — guide someone along a route in real time, using their device's GPS, with automatic rerouting if they go off track.
Everything is built on top of MapLibre GL JS (a free, open-source map renderer) and is fully typed in TypeScript, so your editor can tell you what every function expects and returns.
You don't need to understand MapLibre to use this SDK — the <FyreMap>
component and the hooks below handle it for you.
Full docs (including the underlying HTTP API, not just this SDK) live at fyremaps.com/docs.
Table of contents
- Install
- Get an API key
- Quick start
- Showing a map
- Controlling the map (camera, markers, pins)
- Searching for a place
- Getting directions
- Turn-by-turn navigation
- Good to know
- Docs
- Changelog
- License
Install
npm install @fyremaps/web-sdk maplibre-gl react react-dommaplibre-gl, react, and react-dom are peer dependencies — install
whichever versions your app already uses. pmtiles (used to load map
tiles) is installed automatically as part of this package.
One more step: MapLibre needs its own stylesheet. Import it once, somewhere global (your root layout or global CSS file) — this package doesn't do it for you:
import "maplibre-gl/dist/maplibre-gl.css";Get an API key
Every request to FyreMaps needs an API key. Here's how to get one:
- Go to fyremaps.com and register an account.
- Log in to your dashboard.
- Select a plan.
- Copy your API key from the dashboard.
There are two ways to give the SDK your key, and it matters which one you pick:
| Option | Good for | Why |
|---|---|---|
| pkKey prop | Local development only | Anyone can open dev tools and read a key that's shipped to the browser. |
| resolveAuth | Production | Your real key stays on your server. The browser gets a short-lived, disposable credential instead. |
Quick and dirty (dev only):
<FyreMapsProvider pkKey="pk_test_...">The real setup (recommended for anything users will actually see):
Step 1 — add a small server route that holds your real secret key and hands out short-lived credentials:
// app/api/fyremaps/auth/route.ts — Next.js App Router.
// No server of your own (e.g. plain Vite/CRA)? Deploy this same handler as
// a standalone Vercel/Cloudflare/Netlify function instead, and pass
// `cors: { origin }` since it'll then live on a different origin.
import { createNextRouteHandler } from "@fyremaps/web-sdk/server";
export const { GET } = createNextRouteHandler({
mint: async () => ({
headers: { "x-api-key": process.env.FYREMAPS_SECRET_KEY! },
expiresInMs: 5 * 60 * 1000, // how long the credential you hand out stays valid
}),
});Step 2 — point the SDK at that route:
import { FyreMapsProvider, createFetchAuthResolver } from "@fyremaps/web-sdk";
const resolveAuth = createFetchAuthResolver({ url: "/api/fyremaps/auth" });
<FyreMapsProvider resolveAuth={resolveAuth}>That's it — createFetchAuthResolver automatically caches the credential
and quietly refreshes it before it expires, so this doesn't mean a network
call on every single request. If you want full control you can write your
own resolveAuth function instead, but then caching is on you (wrap it in
createCachingAuthResolver if you want it).
You don't need a proxy to make this work — api.fyremaps.com and
tiles.fyremaps.com are already set up to accept requests straight from
the browser.
Quick start (Next.js App Router) - Initializations
Step 1 — wrap your app in the provider. This is what shares one config/connection with every map and hook in your app.
// app/providers.tsx
"use client";
import { FyreMapsProvider, createFetchAuthResolver } from "@fyremaps/web-sdk";
const resolveAuth = createFetchAuthResolver({ url: "/api/fyremaps/auth" });
export function Providers({ children }: { children: React.ReactNode }) {
return (
<FyreMapsProvider resolveAuth={resolveAuth}>
{children}
</FyreMapsProvider>
);
}(Pair this with the server route from Get an API key above.)
Step 2 — render a map.
// app/map/page.tsx
"use client";
import "maplibre-gl/dist/maplibre-gl.css";
import { FyreMap, PMStyle } from "@fyremaps/web-sdk";
export default function MapPage() {
return (
<div style={{ width: "100vw", height: "100vh" }}>
<FyreMap
style={PMStyle.DEFAULT}
center={[-97, 38]}
zoom={4}
onReady={(map) => {
map.addMarker("home", { lat: 38.9, lon: -77.0 });
}}
/>
</div>
);
}That's a working map with one pin on it. Everything past this point is about the pieces you can add on top: search boxes, directions, live navigation, and more control over what's drawn on the map.
A couple of rules worth knowing up front:
Every hook in this package (
useSearch,useRouting,useNavigation, …) only works inside<FyreMapsProvider>, and only in a Client Component ("use client") — the map and GPS APIs need a real browser.<FyreMap>is the one exception: if all you need is a bare map with no search/directions/navigation, you can skip the provider entirely and passpkKey/resolveAuthstraight to<FyreMap>itself:<FyreMap pkKey="pk_test_..." style={PMStyle.DEFAULT} center={[-97, 38]} zoom={4} />The moment you need one of the hooks, switch to wrapping things in
<FyreMapsProvider>instead — that's what lets<FyreMap>and the hooks share one connection. Don't configure auth in both places at once — if<FyreMap>is inside a provider, the provider always wins, and any auth props passed directly to<FyreMap>are silently ignored (with a console warning in dev, so you notice).
Showing a map
<FyreMap> is a normal React component. style picks one of five
built-in looks:
import { FyreMap, PMStyle } from "@fyremaps/web-sdk";
<FyreMap
style={PMStyle.DEFAULT} // also: DARK, SATELLITE, REAL_SATELLITE, TERRAIN
center={[-97, 38]} // [longitude, latitude]
zoom={4}
pitch={0}
bearing={0}
onReady={(map) => {
// `map` is your remote control for everything else — see below.
}}
/>A couple of notes on the styles:
REAL_SATELLITEuses a third-party imagery provider (Esri) — check their terms of service and attribution requirements before shipping it.TERRAINisn't built yet. Picking it quietly falls back toDEFAULTand logs a console warning, so your app doesn't break — it just won't look like terrain until this ships.
Controlling the map (camera, markers, pins)
Once the map has loaded, onReady gives you a controller — think of
it as the remote control for everything on the map that isn't set by a
prop. Every example below assumes you're inside that onReady callback
(or have stashed the controller somewhere you can use it later).
<FyreMap
onReady={(map) => {
// Move the camera
map.moveCamera(38.9, -77.0, 14); // fly to lat, lng, zoom
map.fitPoints([{ lat: 38.9, lon: -77.0 }, { lat: 40.7, lon: -73.9 }]); // fit both points on screen
map.toggle3DMode(); // tilt the camera for a 3D-ish view, and back
// Add a pin, and react to taps on it
map.addMarker("home", { lat: 38.9, lon: -77.0 }, { color: "#1a73e8" });
map.setOnMarkerClickListener((id) => console.log("tapped:", id));
// Let users long-press (or right-click) the map to drop a pin there
map.setOnMapLongClickListener((location) => {
void map.showDroppedPin(location); // shows the pin + looks up its address
});
// Then read it back with:
// map.getDroppedPinLocation()
// map.getDroppedPinShareText() — e.g. "123 Main St (38.9, -77.0)"
// map.getDroppedPinDirectionsUri() — a ready-to-open directions link
// Draw a route on the map (see "Getting directions" for where `route` comes from)
map.displayRoute(route.shape);
map.displayAlternateRoute("alt-1", altRoute.shape); // drawn in a muted color
map.removeRoute();
// Switch styles later, without re-creating the map
map.applyStyle(PMStyle.DARK);
}}
/>If something goes wrong loading a style (a bad tile URL, for example), it
won't throw — instead map.styleError is a small live value you can watch
if you want to show an error in your UI.
Searching for a place
The simplest way to add a search box is the two hooks below: one that searches as the user types, one you trigger yourself (e.g. on Enter).
"use client";
import { useAutocomplete, useSearch } from "@fyremaps/web-sdk";
function SearchBox() {
const [query, setQuery] = useState("");
// Fires automatically 300ms after typing stops, once there are 3+ characters
const { results, loading } = useAutocomplete(query);
// Fires only when you call `search(...)` yourself
const { search, results: searchResults } = useSearch();
return (
<input value={query} onChange={(e) => setQuery(e.target.value)} />
// render `results?.features` as a dropdown; call `search(query)` on submit
);
}useSearch actually covers five different kinds of lookup, all with the
same loading/error handling:
| Method | What it does |
|---|---|
| search(text) | Free-text search, like a search engine |
| autocomplete(text) | Same, but tuned for "as you type" suggestions |
| reverse(location) | Coordinates → nearest address |
| structuredSearch({ ... }) | Search by individual fields (street, city, postal code, country, ...) instead of one text string |
| place(ids) | Look up specific places you already have the ID for |
Every one of these accepts a second, optional argument to narrow the
results — e.g. "only within 5km of here," "only in this country," "only
venues, not addresses." The full list: focusLat/focusLon (bias results
toward a point), boundaryRect/boundaryCircle/boundaryCountry (only
search inside this area), layers (limit to venues, streets, postal
codes, ...), sources, size (how many results, 1–40), lang.
Need to search from outside a React hook — say, inside a button's onClick
handler? Reach for GeocodingClient directly:
const { geocoding } = useFyreMaps();
const results = await geocoding.searchNear("coffee", 38.9, -77.0, 2 /* km */);
const location = geocoding.getFirstCoordinates(results); // { lat, lon } | nullRemembering what someone searched for last: useSearchHistory saves
picks to the browser's local storage (IndexedDB) and gives you a
live-updating list:
const { entries, addToHistory, clear } = useSearchHistory();
await addToHistory("1600 Amphitheatre Pkwy", 37.422, -122.084);Getting directions
useRouting turns a list of places into a drivable (or walkable, or
cyclable) route.
A → B
const { getRoute } = useRouting();
const route = await getRoute([origin, destination]);
// route.shape — the line to draw on the map
// route.steps — turn-by-turn instructions
// route.totalDistanceMeters / route.totalDuration (seconds)A → B → C → D (multiple stops, in order)
This is the same function — just hand it more than two places. The route visits them in the order you listed, so put your stops in the order you want them visited:
const stops = [origin, warehouseA, warehouseB, destination];
const route = await getRoute(stops, RoutingProfile.AUTO, { avoidTolls: true });"I have 4 stops, but I don't care what order" (route optimization)
If you don't know the best order to visit your stops in, let the routing engine figure it out for you — it picks the order that minimizes total travel:
const { getOptimizedRoute } = useRouting();
const result = await getOptimizedRoute([depot, stopA, stopB, stopC]);
// result.route — a ready-to-use route, same shape as getRoute() returns
// result.order — the visiting order it picked (treat as provisional —
// not yet verified against a live backend)Other things useRouting and RoutingClient can do
Different travel profiles: pass
RoutingProfile.AUTO(default),TRUCK,BICYCLE,PEDESTRIAN, orMOTORCYCLEas the second argument.Avoid tolls/highways/ferries: pass
{ avoidTolls: true }etc. as route options.Alternate routes:
getRoutes(locations, profile, alternates)returns up to N routes instead of one — draw the non-primary ones withmap.displayAlternateRoute(...).Truck-specific routing (height/width/length/weight/hazmat limits), isochrones (draw "everywhere reachable in 10 minutes" as a shape on the map), and distance/time matrices (travel time between many points at once) aren't wrapped as hooks yet — call them straight off
RoutingClient:const { routing } = useFyreMaps(); await routing.getTruckRoute(stops, { heightMeters: 4.1, weightKg: 26000 }); await routing.getTimeIsochrone(origin, RoutingProfile.PEDESTRIAN, [5, 10, 15]); await routing.getMatrix(sources, targets);
All distances on a route (totalDistanceMeters, each step's distance)
are in meters, and durations are in seconds.
Turn-by-turn navigation
This is the "blue dot following you down the highway" feature: live guidance along a route, using the device's real GPS position.
It's built from three pieces working together:
useRouting— gets you the route.useNavigation— tracks progress along that route and tells you what to say next ("turn left in 200m").useGeolocation— reads the device's actual GPS position and feeds it into navigation.
"use client";
import { useEffect } from "react";
import { useGeolocation, useNavigation, useRouting } from "@fyremaps/web-sdk";
function NavigationDemo(origin: Location, destination: Location) {
const { getRoute } = useRouting();
const { startNavigation, guidanceState } = useNavigation();
// Watches the device's position and automatically feeds it into navigation
useGeolocation({ feedNavigation: true });
useEffect(() => {
getRoute([origin, destination]).then((route) => route && startNavigation(route));
}, []);
return (
<div>
{guidanceState.currentRoadName} — {guidanceState.remainingDistanceM}m remaining
</div>
);
}That's it for the basic case. A few things happen for you automatically:
- Off-route detection. If the person strays more than 50 meters from the route, navigation notices.
- Automatic rerouting. When that happens, it silently fetches a fresh route from the current position — no user action needed.
- Live progress.
guidanceStateupdates continuously with everything you'd want to show in a UI (see the table below).
Navigating through multiple stops
Pass your in-between stops as waypoints — the third argument to
startNavigation (leave out the origin and final destination; the
destination is already implied by the route):
const stops = [origin, stopA, stopB, destination];
const route = await getRoute(stops);
if (route) startNavigation(route, RoutingProfile.AUTO, [stopA, stopB]);Navigation keeps track of which stops have already been reached (within about 50 meters). If it has to reroute mid-trip, it's smart about it — the new route only goes through whichever stops haven't been visited yet, so a reroute never sends someone back to a stop they already passed.
Everything in guidanceState
| Field | What it tells you |
|---|---|
| navigationPhase | Where things stand: STARTING → ACTIVE → ARRIVING/ARRIVED, or OFF_ROUTE/REROUTING if things went sideways |
| currentStepIndex / totalSteps | Which turn-by-turn instruction is current |
| distanceToNextManeuverM | Meters until the next turn |
| remainingDistanceM / remainingDurationS | Meters/seconds left in the whole trip |
| etaEpochMillis | Estimated arrival time |
| currentRoadName / nextRoadName | Street names, for display |
| snappedLocation / snappedBearing | The device's position "snapped" onto the route line, and which way it's facing |
| progressFraction | 0 to 1, how far through the trip |
| speedMps | Current speed, if the device reports it |
You can adjust how sensitive things are with setOffRouteThresholdM
(default 50m) and setArrivalThresholdM (default 20m), either before
starting or mid-trip.
Good to know
A few things that are worth knowing about before you rely on them:
hasTolls/hasHighways/hasFerrieson a route come straight from your routing backend — double-check they're actually populated for your deployment before building UI around them.- Remaining ETA/duration during navigation is a straight-line, distance-proportional estimate — not a model that accounts for traffic or per-step timing.
- No offline maps, no background navigation, no picture-in-picture.
Browser tabs get throttled once they're not visible, so
useGeolocationonly reliably works while the tab is in the foreground. REAL_SATELLITEstyle uses Esri World Imagery — check their terms of service and attribution requirements for your use case.TERRAINstyle isn't implemented yet — it falls back toDEFAULTwith a console warning rather than breaking.
Everything mentioned in this doc is exported from the package root —
import { ... } from "@fyremaps/web-sdk" — with one exception:
@fyremaps/web-sdk/server (createFetchHandler / createNextRouteHandler)
is a separate entry point on purpose, so your real secret key and other
server-only code never ends up inside a browser bundle.
Docs
Full documentation — including the underlying HTTP API this SDK talks to — lives at fyremaps.com/docs.
Changelog
See CHANGELOG.md.
