Skip to main content

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.

NamespaceAffects
map.selectionWhich building, floor, or room is selected
map.cameraWhere the viewport is looking, and how it moves
map.directionsThe route currently being drawn
map.uiWhich 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.

Examples use a demonstration campus. Every snippet on this site addresses 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:

SourceMeaning
userA visitor gesture in the map
hostA command your page sent
systemThe 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