How the embed works
Everything else in these docs assumes the model on this page. It is short, and reading it first will save you from a class of confusing symptoms later.
Two programs, two origins
An embed is two programs running in one browser tab.
The map is served by Waylocate and runs in its own browsing context on Waylocate's origin. It owns the rendering, the campus data, the floor plans, and the visitor-facing interface.
The host is your page. It owns the container the map sits in, and it holds a handle to the map.
The two cannot touch each other's DOM. Browsers isolate documents from different origins, and that isolation is the point rather than an obstacle: it is what lets Waylocate ship map changes without breaking your page, and what stops a map bug from reaching your application state.
All communication therefore happens by passing messages, and every capability in these docs is a message that has been given a name and a payload shape.
The handle
Whichever integration you choose, you end up holding the same object: a
WaylocateMap. It is the only thing you need to keep a reference to.
const map = await mount({ container: "#map", campus: "ucba" });
The handle groups commands into namespaces that match what they affect.
| Namespace | Affects |
|---|---|
map.selection | Which building, floor, or room is selected |
map.camera | Where the viewport is looking, and how it moves |
map.directions | The route currently being drawn |
map.ui | Which interface elements the map shows |
map.getState() | A snapshot of all of the above |
map.on() | Subscriptions to things the map reports |
Commands are asynchronous because they cross a boundary. Each one carries a correlation identifier, and the map answers with a result carrying the same identifier. The SDK turns that acknowledgement into a resolved promise, and an error result into a rejection.
try {
await map.selection.setRoom("muntz-hall/999");
} catch (error) {
// error.code === "REF_NOT_FOUND"
}
This is worth stating plainly because the alternative design is common and worse: a command that returns nothing leaves you unable to tell a typo from a slow network.
References name places
Buildings, floors, and rooms are addressed by stable slugs rather than database identifiers. The same grammar is used in URLs, in commands, and in event payloads, so a value you receive can be sent straight back.
muntz-hall a building
muntz-hall/floor/2 a floor
muntz-hall/170 a room
here the visitor's current position
pin:39.1329,-84.5150 an arbitrary coordinate
Internal identifiers are deliberately absent from the protocol. A reference that
does not resolve produces a REF_NOT_FOUND error rather than failing quietly.
The full grammar is documented in the reference grammar.
ucba, a publicly available campus with the building muntz-hall and the room 170. Substitute your own campus slug once you have one. Slugs for your campus are listed in your Waylocate console, and the addressing rules are in the reference grammar.Every event says who caused it
The map reports changes regardless of what caused them. A visitor tapping a room
and your own selection.setRoom call both produce a selection.changed event.
Each event carries a meta.source:
| Source | Meaning |
|---|---|
user | A visitor gesture in the map |
host | A command your page sent |
system | The map's own behaviour, such as framing a newly selected building |
Without this field, a host that updates its own state from map events and also
sends commands would feed itself in a loop. Checking meta.source === "user" is
the normal way to avoid that.
The visitor is never locked out
Commands do not put the map into a controlled mode. If you fly the camera somewhere and the visitor then pans away, the visitor wins. The map also continues to reframe itself when its own behaviour calls for it, such as refitting a route when the drawer resizes.
Build on the assumption that your last command describes an intent that has
already been superseded, not a state you own. If you need to know where things
actually stand, read map.getState() or listen for state.changed.
The two sides version independently
You pin a version of @waylocate/embed in your package.json. Waylocate
deploys the map. Those two events are not coordinated, so the map your
integration talks to is usually newer than the SDK that was installed against
it.
Three mechanisms keep that safe.
The protocol only ever grows. Commands, events, and payload fields are added but never renamed, repurposed, or removed. An older SDK keeps working; it simply has no method for commands introduced after it shipped.
The map advertises what it can do. During the handshake it sends a list of
the commands this build accepts. The SDK checks that list before sending, so
calling something the map does not have fails immediately with UNSUPPORTED
rather than waiting for a timeout. You can check the same list yourself:
if (map.supports("camera.orbit")) {
await map.camera.orbit({ ref: "muntz-hall", secondsPerRevolution: 20 });
}
Unknown fields pass through. The SDK does not strip payload fields it does not recognise, which is what lets an old build keep receiving events that have since gained new data.
What is covered by which promise is set out in compatibility.
Only one origin may drive the map
The embed URL carries a host parameter naming the single origin allowed to
send commands. The map compares the sender of every incoming message against it
by exact string equality, and drops anything that does not match without
replying.
When you use mount() or the React component, this is filled in from your
page's origin and you never see it. It matters when you build the embed URL
yourself, because a missing or malformed host produces a map that renders
perfectly and ignores every command. See
URL parameters.
Next
- Integration to choose how you embed.
- Controlling the map for the full command surface.