Model data

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:

GET /api/model-data
Session present, IDs match [a-zA-Z0-9_-]+
Resolve the version through GraphQL

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.
Every response is marked 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.

Reading a bundle in Node

Artifact manifest

Fetched with the account token.

Artifacts

Pre-signed URLs, fetched over https only and without the token. A running byte count aborts an oversized file mid-download.

Parquet tables

Read with hyparquet and its compressor pack.

SGEO v1 geometry

Magic bytes, version and a CRC32 checksum are verified. Quantised geometry and unknown unit codes are rejected, not guessed.

BundleLoader

Backed by 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.

SGEO v1 geometry support
Decoded
  • Meshes
  • Lines
  • Polylines
  • Point clouds
  • Display polylines for NURBS curves and spirals
Not implemented
  • Instance placements and definitions
  • Other analytic primitives
  • Model reference-point transforms
Partially unsupported models show a visible warning; entirely unsupported ones return an explicit error.