URDF & Meshes
This is the asset store — the mechanism, not the rendering recipe built on top of it (see the end of this page for that).
An asset is an immutable file, not a service kind
An asset is an immutable file that belongs to one robot — not a service
type like a datapoint or an action. Every asset has a UUID and is
fetched by it. There are no org-level assets and no public fetch: every
access is authenticated and role-checked (the assets right, see
Apps, End Users & Roles).
One authorization mechanism: the Authorization header
Assets use exactly the same bearer token as every other API call — no token in the URL, no cookie, no signed URL. That’s a deliberate choice about the number of mechanisms, not ergonomics: a second credential would carry its own expiry, its own renewal, its own rotation, and every log entry would need to say which kind of token it was. Instead assets inherit token renewal, role checks, audit and rate limiting from the mechanism that already exists.
Consequence, stated explicitly: this is not a CDN. A shared cache must
not store a response to a request carrying Authorization. Assets are
immutable, so Cache-Control: private, immutable is correct and a
browser’s own cache works fully — an edge cache in front does not.
Signed URLs remain possible as a later, purely additive endpoint, precisely
because assets are immutable and UUID-addressed.
URDF is an asset with a special role
The URDF is copied along with a robot’s configuration and is the entry point through which meshes are found.
Two ways an asset enters the store:
- A connected bridge detects the URDF (default source
/robot_description) and reports only that it’s available — nothing transfers until the developer explicitly clicks Sync in the console. This spends the org’s asset quota, which is why it’s a developer/console action rather than something your client app can trigger. - A developer manually uploads a zip (URDF + meshes).
Meshes (package:// URIs) are resolved by the bridge against its workspace
and uploaded on request. Storage is content-addressed — identical
meshes across robots cost one copy — and counts against the org quota. The
console reports completeness (“2 meshes missing”).
The served URDF is re-serialized, not byte-identical
GET .../urdf parses the document and returns it re-emitted: XML
comments and the original attribute/element order are not preserved.
Everything semantics-bearing survives — namespaces, xacro: prefixes,
attribute values, structure, unicode names, empty elements — but a license
or attribution header written as a comment is gone after fetch, and a diff
against the original file is not meaningful. If you need the original
bytes, fetch the URDF asset directly by its UUID — the store keeps that
copy untouched; rewriting only happens on the .../urdf route.
GET .../urdf also rewrites mesh package:// URIs into Fleetless asset
URLs your client can fetch with the same bearer token.
Rendering is your app’s job, and the SDK carries the one piece you can’t build yourself
3D rendering (three.js + urdf-loader) is deliberately not a platform
feature — it’s a documented recipe. But there’s one piece of glue nothing
else can substitute for: neither a plain <img> tag nor urdf-loader’s
default loaders can set an Authorization header, and every asset byte on
Fleetless requires one. The SDK ships a mesh-loading callback
(assets.createMeshLoader) and a fuller scene-preparation helper
(assets.prepareUrdfScene, which also handles a <material>'s texture and
an image a .dae references internally) so every app doesn’t write this
fetch-with-header glue itself, slightly differently each time.
Joint states are an ordinary datapoint (typically a whole-topic subscription
on /joint_states) — nothing special about them.
For the full step-by-step, worked code, and a verified browser run,
including the traps found along the way (an unmapped mesh reference must
refuse rather than silently fetch cross-origin; a .dae’s relative texture
path needs a second normalized lookup), see the
Live 3D recipe.