Architecture
The stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router), React 19 |
| Language | TypeScript, strict |
| Styling | Tailwind CSS v4 with CSS-first @theme tokens |
| Components | Radix UI primitives, shadcn-style wrappers in components/ui |
| Server state | TanStack Query v5 |
| 3D — models | @speckle/viewer, @speckle/objectloader2 |
| 3D — landing | three.js with React Three Fiber |
| Columnar data | hyparquet and hyparquet-compressors |
| Package manager | pnpm |
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:
proxy.ts
Is the Speckle session cookie present? Presence only: no token validation and no network call in front of navigation.
- app/(Landing)
//about
- app/(auth)
/login
- app/(Dashboard)
/dashboard/scene/[id]/account/[section]
- app/api
/api/speckle-connect/api/speckle-disconnect/api/model-data/api/check/api/session-expired
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.