This is 2026.9 documentation. Coverage here is incremental: a page exists only where 2026.9
differs or is newly documented. For any other topic, switch to Current (2026.8 and earlier) in
the version selector.
@speckle/viewer and loads Speckle data itself, a 2026.9 version
gives it nothing to draw: the version’s data is a bundle
of parquet files, and the objects endpoints return 404. This page is the bridge. You add
about a dozen files to your own app, keep the same two published packages you already
depend on, and the viewer renders bundles.
Nothing is patched and nothing new is published. You read the bundle, project it onto the
object shape the viewer already converts, and hand those objects to the loader.
Check whether this affects you
Read a version record before assuming either shape.
You need this page if your app constructs a viewer and a loader itself. You do not need it
if you embed the hosted web viewer in an iframe, or if you only read data through the
GraphQL or REST API.
Compatibility mode keeps a workspace’s data in the object-graph format until the mode ends. See
Compatibility mode.
What the bridge does
- Reads the version record and dispatches on its shape.
- Lists the version’s artifacts and downloads the parquet tables it needs.
- Decodes SGEO geometry.
- Projects the bundle onto
Baseobjects, with collection, material, colour and instance proxies. - Hands those objects to
ObjectLoader2Factory.createFromObjectsthrough aSpeckleLoadersubclass.
What the bridge costs you
That last sentence is the trade. The bridge reconstitutes the object graph 2026.9 retires: it reads the whole bundle, buildsBase objects in memory, and hands them to the converter
that has always run. So it inherits the load time and memory profile you have today, and it
gives up most of what the bundle format exists to enable — streaming reads, sharded geometry
fetched on demand, dense integer keys, properties queried without materialising objects, and
the server-built viewer artifact.
Read the bridge as what keeps your app working across the format change, not as where it
should stay. It is at its best on models of the size you render well today; it will not make
a model that is slow now fast, and on very large models it can be worse than the object graph
was, because the whole bundle is read before anything draws.
1. Pin both packages exactly
Pin@speckle/viewer and @speckle/objectloader2 to the same version, with no carets. The
bridge depends on internals of neither, but it does depend on initObjectLoader being
overridable and on createFromObjects existing, and a range would let a future release move
either.
package.json
2.31.14 in your lockfile.
2. Add a parquet reader and a zstd codec
A bundle is parquet, and the files are zstd-compressed. Both dependencies go in yourpackage.json, not in a Speckle package.
content values are byte arrays, not
strings.
3. Drop in the projection
Copysrc/bridge/ from the sample into your app. projectBundle turns the bundle into the
object shape the viewer already converts: a collection tree, one object per applicationId
carrying its display geometry, and instance, material and colour proxies.
Rendering is unconditional: geometry, nested instances, grouping, and the full material and
colour precedence from the geometry up through the object to its container. Two things cost
extra reads, so they are options rather than defaults:
properties merges each object’s instance and type-level rows — the type rows live in their
own table, and without that merge most Revit type parameters are simply absent. referencePoint
carries the model’s datum onto the root. An app that only draws leaves both off and never
downloads the eav tables at all.
You should see projectBundle return a root object whose elements hold your objects.
4. Swap the loader
Both shapes of version end atviewer.loadObject. The only difference is which loader gets
there, so dispatch on schemaVersion and keep your existing path intact.
Assume viewer, the project, model and version ids, and a token are already in scope.
- Prior to 2026.9
- 2026.9
Prior to 2026.9
resourceData through to initObjectLoader, which is how the
projected objects reach the override before any subclass field exists — the same route
SpeckleOfflineLoader takes.
bundleLoader.ts
viewer.getWorldTree() report nodes.
Your own extensions need no changes. They are created the same way, see the same world tree,
and resolve the same ids. The sample proves that with two the Speckle web app never runs:
ExplodeExtension, which ships with the viewer, and a box select written from scratch —
extensions/boxSelect.ts
5. Rekey your interactions to applicationId
Everything above is copy-paste. This step is your app’s own data model, and skipping it is what makes a load look successful while your saved state silently matches nothing. Before 2026.9, a Speckle object’s identity was a content hash, stable across versions of the same unchanged element and usable as a durable key. In a bundle there is no hash. The object’s identity isapplicationId — the id the authoring application gave the element.
The bridge sets id to applicationId on every object, so the ids the viewer hands you in
selection events, queries and filtering state are applicationId values.
What that changes:
- Anything you persisted keyed by object id needs a migration or a rekey. Saved selections, comment anchors, your own annotations, cached per-object state. A stored content hash will not match anything in a bundle.
applicationIdis stable across versions in a way hashes were not. An element that moved still carries the sameapplicationId, where its hash changed. For most apps this is better: your per-object state survives a republish.- Synthetic ids are per-bundle. Anything the projection mints rather than reads —
containers (
coll-7), definitions (def-3), materials (mat-0) — is local to one bundle and will differ in the next version. Never persist those. - Duplicate
applicationIdvalues are possible. Producers are asked for stable unique ids and mostly deliver them, but a host with no stable id per element can repeat one. Decide whether last-wins or a reported conflict is right for your app.
applicationId.
What does not come across
A bundle carries far more than the projection reads, and more than the published viewer can draw. None of these raise an error, so check the list against what your app promises. Relations. The catalog is in Relations. The projection reads DISPLAY, DEFINES, HAS_MATERIAL, HAS_COLOR, DISPLAY_INSTANCE, DEFINES_INSTANCE, IN_COLLECTION and the object and node appearance relations. Everything else is carried and unread: solids, centrelines, scene views, levels, rooms, systems, groups, assemblies, connectivity and hosting. Those rows are in the bundle and available to you — nothing in the sample reads them into the tree. Geometry. Meshes, lines, polylines and the tessellated curve family render. Text, points, boxes and regions decode but are not converted. The sample counts what it skipped and prints the tally, so a model that is mostly annotation looks empty for a stated reason. Vocabulary you do not know. Read columns by name and tolerate columns you do not know, and treat a relation id or node kind your reader does not recognise as skip-and-report rather than an error. A producer newer than your copy of the bridge is normal, and the sample surfaces those ids for you. The sample is verified against@speckle/[email protected], @speckle/[email protected]
and bundle spec 1.2.0, as of September 2026. Check those pins before assuming a behaviour
still holds.
Viewer concepts that still apply
The bridge changes where data comes from, not how the viewer works. The loader and tree documentation still applies:Loaders
What a loader is and where it sits in a load.
Loader API
The abstract
Loader contract the subclass implements.World tree API
What the converter builds, and how to query it.
Compatibility
Earlier viewer migrations, for context on what changed before.
FAQ
Why is there no published package for this?
Why is there no published package for this?
Publishing it would make a third implementation of the receive fidelity contract that Speckle
then has to keep in step with the .NET and Python SDKs. Sample code you own is honest about what
it is: a worked example of documented rules, dated, in your app, under your control.
Can I skip downloading geometry?
Can I skip downloading geometry?
Yes — pass
includeGeometry: false and the geometry shards, which are the bulk of a bundle, are
never fetched. The projection here renders, so that leaves it with nothing to draw; it is useful
once you extend the projection to read the eav tables.Can it use my browser session instead of a token?
Can it use my browser session instead of a token?
Not from another origin. Your app cannot read Speckle’s auth cookie — that is the origin model —
and sending the request with
credentials: 'include' does not help either, because the cookie
is SameSite=Lax and so never rides a cross-site fetch. Use a token. A public project needs
none at all: send no Authorization header rather than an empty one, which is rejected.Do I still need @speckle/objectloader2?
Do I still need @speckle/objectloader2?
Yes, and you keep the same version. The bridge uses
ObjectLoader2Factory.createFromObjects
instead of createFromUrl, which is why no request goes to the objects endpoints.What happens if the artifacts listing returns 404?
What happens if the artifacts listing returns 404?
Either the version is an object graph from 2026.8 or earlier, the bundle has not been produced
yet, or your token cannot read the project. Check
schemaVersion on the version record first,
then the ingestion status if the version is new.My app reads object graphs with objectloader2 and never touches the Viewer. What do I do?
My app reads object graphs with objectloader2 and never touches the Viewer. What do I do?
Read the bundle directly rather than bridging to an object graph. Start from Building
applications, which points at the load mechanics for
data-only apps.
The console logs “Consumable applicationId … could not be found”.
The console logs “Consumable applicationId … could not be found”.
A nested instance — a definition placed inside another definition — logs this once per nested
placement. The converter looks the node up after the parent definition has already consumed it.
The tree is still correct: the nested definition expands and renders under its transform. What
it does skip is the container-colour fallback for that nested placement.
The model renders but everything is at the origin, stacked.
The model renders but everything is at the origin, stacked.
Instance placement is not being applied. Check that your projection emits an
InstanceProxy per
DISPLAY_INSTANCE edge with the instance node’s transform, and that objects carrying a
definition member’s properties are not also drawn — a carrier with geometry draws twice, once
untransformed at the origin.