Architecture

Architecture

The stack

LayerChoice
FrameworkNext.js 16 (App Router), React 19
LanguageTypeScript, strict
StylingTailwind CSS v4 with CSS-first @theme tokens
ComponentsRadix UI primitives, shadcn-style wrappers in components/ui
Server stateTanStack Query v5
3D — models@speckle/viewer, @speckle/objectloader2
3D — landingthree.js with React Three Fiber
Columnar datahyparquet and hyparquet-compressors
Package managerpnpm

Two details worth calling out. Development runs on Turbopack, but production builds are pinned to webpack via next build --webpack. And next.config.mjs pins experimental.cpus to 2, because Next derives its static-generation worker count from os.cpus(), which in a container reports the host's cores rather than the container's CPU limit — each worker is a full Node process, so an unpinned count is what pushes CI builds into the OOM killer.

How the app is laid out

Routing uses App Router route groups, which organise files without adding URL segments. A request passes the proxy guard first, then lands in one of four groups:

Request path and route groups
Request

proxy.ts

Is the Speckle session cookie present? Presence only: no token validation and no network call in front of navigation.

  • app/(Landing)
    • /
    • /about
    Public
  • app/(auth)
    • /login
    Signed-in visitors are sent away
  • app/(Dashboard)
    • /dashboard
    • /scene/[id]
    • /account/[section]
    Anonymous visitors are sent away
  • app/api
    • /api/speckle-connect
    • /api/speckle-disconnect
    • /api/model-data
    • /api/check
    • /api/session-expired
    Route handlers check the session themselves
Folder names in parentheses organise files without adding a URL segment.

Each group owns a layout.tsx, so the dashboard can mount its providers and sidebar without the landing page paying for any of it.

Route protection lives in proxy.ts. In Next 16 this file replaces what used to be middleware.ts — same position in the request path, new name. The guard is deliberately thin: it checks for the presence of the Speckle session cookie, redirects anonymous visitors away from /dashboard, /account and /scene, and bounces already-signed-in users off /login. It does not attempt to validate the token; that is the API's job, and doing it here would put a network call in front of every navigation. A token Speckle refuses is caught instead by the requests the app makes anyway, which end the session through /api/session-expired; see When a session ends. The /login bounce is skipped when the URL carries a session notice, so a session that could not be confirmed can always sign in again.

The landing scene

The front page renders a Speckle-figure GLB through React Three Fiber. It is deliberately small: one mesh, two lights, no controls.

Getting it to behave took more care than the triangle count suggests. The model is fitted against its bounding sphere rather than its bounding box, because a sphere is rotation-invariant — the idle animation can swing to any angle without clipping, and the fit adapts to viewport aspect so a phone does not get a figure cropped off the right edge. The render loop runs continuously rather than on demand, which costs a little battery but removes a real failure mode: a single on-demand frame is lost permanently if the drawing buffer is dropped, and the scene then stays blank with nothing to redraw it. Device pixel ratio is allowed up to 2 so the silhouette's long smooth edges stay crisp on retina displays, and the whole animation freezes under prefers-reduced-motion.