04 · Case study
New polyglot monorepoNext.js 16 · React 19 · Rust 1.96 · axum · PostgreSQL 17
The system
The problem
A viewer for 3D scans. Someone uploads a few hundred megabytes of mesh and expects to rotate it in the browser a second later, so something between the upload and the first frame has to parse a binary format, decimate the geometry into levels of detail, tile it, cache it and stream it. Both single-runtime answers are bad: decode in Node and you spend the project fighting a garbage collector that was never meant to hold a 400 MB vertex buffer; write the interface in Rust and you have given up React to avoid a language boundary you were always going to cross. The right answer is both, and the reason teams do not take it is that both costs a week of monorepo, two toolchains, a contract between them and a pipeline that can build each without knowing about the other — before a line of product code exists.
What ran
Eleven sections: project, architecture, UI, data, auth, state, testing, caching, CI, deploy, config. Every one of them is a target decision — a thing no parser could derive, because the code does not exist yet. You are choosing between block sets that have already been written and tested, not describing a wish.
The answers select blocks and the blocks are assembled. Nothing here is generated prose: run the same answers twice and you get the same tree, file for file.
Next.js’ built-in cache is right on one instance. More than one replica and the replicas need to share, so the run wires a Redis cache handler instead. It is a question, up front, because retrofitting it after the viewer works is a rewrite of how every page is served.
A real structural pass over the scaffold on disk — not a memory of what it intended to write — and the planning context is built from that.
The upload endpoint, the decode pipeline, the level-of-detail generation, the tile store, the streaming route, the canvas component, the camera controls. Written down before anything is created, and yours to read.
cargo build on the Rust side, tsc on the TypeScript side, between every task. Each passing task is its own commit. A failing task keeps the run on that task rather than stacking a second broken change on the first.
What it did
What lands
apps/web Next.js 16 on React 19, App Router, shadcn/ui on Tailwind v4, TanStack Query with Zustand, Vitest and Playwright wired · services/api Rust axum 0.8 on tokio, sqlx against PostgreSQL 17, figment config, thiserror, tracing exporting OpenTelemetry · packages/ui and packages/config so the two halves cannot drift on lint rules or on what a button looks like.
The contract has a direction
The Rust service owns the OpenAPI spec and emits it to contracts/openapi/; packages/api-client/ is generated from it. Not both sides hand-writing types against a shared document — which is how a polyglot repository goes wrong, because both sides stay “correct” against a spec nobody regenerates until staging. This way a breaking change in Rust is a TypeScript compile error on the next build.
Delivery, on day one
A distroless non-root Docker image, Kustomize overlays for Kubernetes, and GitHub Actions that build both runtimes, run both suites, and produce an SBOM with Syft that Grype then scans. Plus the documents nobody writes on day one: ARCHITECTURE, CALL-GRAPH, SEQUENCE-DIAGRAMS, STATE-FLOW, RUNBOOK, SECURITY, TESTING, PERFORMANCE, ACCESSIBILITY.
Where it ended
A security scan over dependencies and secrets, a second structural pass over the generated application, a check against the conventions the playbook holds for both runtimes, and a build — a real one, not a claim that it should compile. What you review is a branch with one commit per verified task and the file-level plan you approved before any of them ran.
What happens next
A viewer is a good test of this shape because the hard part is small and specific. The decoder is difficult and it is yours. The monorepo, the contract, the container, the pipeline and the documents are none of those things — they are the same here as they would be for a mapping service or an audio editor, and paying a model to reinvent them each time is how a build gets expensive and inconsistent at once.