Model data
Reading model data
/api/model-data is the only substantial server-side endpoint. It resolves a model version through GraphQL, then decides how to get its properties. Everything it returns is marked Cache-Control: private, no-store.
It is defensive in a few specific ways: it rejects identifiers that are not [a-zA-Z0-9_-]+ before they reach a query, it returns 401 rather than an empty result when there is no session, it verifies that an explicitly requested version actually belongs to the requested model, and it puts explicit timeouts on both the GraphQL call and the property export.
From there, one of four paths is taken:
[a-zA-Z0-9_-]+one of four paths
- Bundle
- When
- The version's referenced object is a bundle.
- Then
- Read the Parquet artifacts and SGEO geometry directly.
- Export
- When
- The version has an EAV property export.
- Then
- Fetch it and normalise the rows into records.
- Viewer
- When
- The export is missing, which is common for older versions.
- Then
- Fall back to the object tree the viewer already loaded.
- Unavailable
- When
- The export 404s on a version that should have one.
- Then
- Say so plainly instead of reporting an empty model.
Cache-Control: private, no-store.That last distinction matters more than it looks. A model with no property data and a model whose property data failed to arrive look identical in a table, and quietly showing zero elements for the second case is how you get someone making a decision on absent data.
Normalising properties
Export schemas vary between connectors and server versions, so normalizeEav accepts several shapes — arrays, or objects wrapping rows/data/items/elements — and handles both one-row-per-property EAV layouts and already-nested records. Rows it cannot understand are skipped individually; one unsupported row must never discard the rest of the model.
Units are carried alongside values rather than folded into them, and a missing unit stays missing. Nothing is inferred from a property's name and nothing is converted.
Native bundle loading
This is the most involved part of the app. Newer Speckle versions store their data as a bundle: an artifact manifest pointing at Parquet tables plus geometry encoded in Speckle's binary SGEO format. Clone reads both in Node rather than handing the work to an external viewer.
Artifact manifest
Artifacts
https only and without the token. A running byte count aborts an oversized file mid-download.Parquet tables
hyparquet and its compressor pack.SGEO v1 geometry
BundleLoader
ObjectLoader2.createFromObjects, so bundle and legacy models share one render path and one set of object IDs.The Parquet side uses hyparquet with its compressor pack. The manifest is fetched with the account token; the artifact URLs it returns are pre-signed, so they are fetched without forwarding that token, and only over https. Files are streamed with a running byte counter so an oversized artifact is aborted mid-download rather than after.
The geometry side is a hand-written SGEO v1 decoder following Speckle's published specklepy schema (see the geometry schema). It validates the SGEO magic bytes and version, verifies a CRC32 checksum over the payload, and rejects quantised geometry and unknown unit codes instead of guessing. Decoded objects are fed into the normal Speckle renderer through a BundleLoader subclass backed by ObjectLoader2.createFromObjects (see the viewer's loaders), so bundle models and legacy models share one render path and one set of object IDs.
The limits are explicit and enforced. Larger bundles would need a streamed or worker-based pipeline, which is not built.
128MB
of artifacts
256files
per bundle
1Mrows
per table
120s
deadline, also aborted if the client disconnects
Supported geometry
Only part of the SGEO geometry vocabulary is decoded. Partial geometry should not be used for spatial analysis.
- Meshes
- Lines
- Polylines
- Point clouds
- Display polylines for NURBS curves and spirals
- Instance placements and definitions
- Other analytic primitives
- Model reference-point transforms