// ai summary · architecture·22 Jun 2026·5 min read

Write the algorithm once. Run it everywhere.

The geotechnical engine behind Terraforge is one Rust workspace that compiles to a native binary for the server and a WebAssembly module for the browser. Same code, same types, two runtimes. And it's the language I'd most want an LLM writing — because the compiler won't let it bluff.

AI summary —written by an AI agent from a brief I gave it. The architecture is real and shipping at Terraforge; the prose isn't hand-written.
RustWebAssemblywasm-bindgenLLM codegen

// the duplication problem

Any product with real domain logic eventually wants to run that logic in two places. On the frontend, so the user gets an instant answer as they drag a slider — no round-trip, works offline, no server cost per keystroke. On the backend, so you can validate, batch, generate reports, and not trust a number a browser computed.

The lazy answer is to write it twice — once in TypeScript, once in whatever the server speaks. Now you have two implementations of a bearing-capacity formula that must agree to the decimal, and they will quietly drift apart the first time someone fixes a bug in one and forgets the other. For a geotechnical engine, "quietly disagree about a foundation's safety factor" is not an acceptable failure mode.

// the insight

One language that targets both runtimes.

Rust compiles to native code and to wasm32-unknown-unknown — WebAssembly that runs in every browser, in Node, in Workers, in Wasmtime on a server. So you write the algorithm once, in a strongly-typed language, and ship the exact same compiled logic to both sides. There is no second implementation to drift.

The Terraforge compute core is ten pure-Rust crates — bearing capacity, CPT correlations, settlement, slope stability, pile capacity, a 2D FEM solver, and so on. None of them know what WebAssembly is. They're just functions over typed structs with serde on the boundary. A single thin facade crate wraps them for the browser:

crates/
├── core/          shared types — soil, CPT, units, geometry
├── bearing/       Terzaghi / Meyerhof / Hansen / Vesic
├── cpt/           Robertson soil-behaviour-type + correlations
├── settlement/    elastic + consolidation
├── slope/         Bishop / Fellenius
├── fem/           2D plane-strain solver
│   … six more pure-compute crates …
└── wasm-bridge/   the ONLY crate that knows about the browser

// the boundary

The facade does one boring thing per function: take JSON in, call the compute crate, hand JSON back. Strings at the edge, typed Rust in the middle.

#[wasm_bindgen]
pub fn bearing_capacity(input_json: &str) -> Result<String, JsValue> {
    let input: BearingInput = serde_json::from_str(input_json)?;  // parse
    let result = terraforge_bearing::solve(&input);              // pure Rust
    Ok(serde_json::to_string(&result)?)                          // serialize
}

On the frontend that's two lines of TypeScript:

import init, { bearing_capacity } from "./pkg/terraforge_wasm.js"
await init()
const out = JSON.parse(bearing_capacity(JSON.stringify(input)))

On the backend you skip the WASM entirely and call terraforge_bearing::solve directly — it's the same crate, now linked into a native binary instead of a .wasm. The browser and the server are running byte-identical logic, because it's the same source compiled twice. JSON strings at the boundary are a deliberate choice: language-neutral, so a Python or Go backend can speak to the same module without caring it's Rust underneath.

// why rust is good with LLMs

The compiler is a reviewer that can't be sweet-talked.

People assume Rust is a worse fit for AI codegen because the borrow checker is famously strict. It's the opposite. An LLM writes plausible code — that's the whole risk. Plausible Python runs, looks right, and is wrong in a way nobody notices until production. Rust's type system and borrow checker reject a huge class of "looks right, is wrong" before it ever runs.

That makes the loop tight and self-correcting. The model proposes, the compiler objects with a specific, located error, the model fixes it. The pain that Rust is famous for inflicting on humans is exactly the signal an agent needs: a fast, precise, unforgeable "no." No null slipping through. No silent type coercion. Exhaustive match arms the model can't forget. The compiler does the review a human reviewer is too tired to do at 2am.

// why not just ship python

"But the algorithms already exist in Python." Sure — and you can run Python in the browser now, via Pyodide (CPython compiled to WASM). I tried it. It works, and it brings every disadvantage of Python with it.

  • Heavy. The Pyodide runtime is ~10 MB+ before your code or NumPy. The Terraforge Rust module is a single small .wasm — kilobytes of logic, not a whole interpreter.
  • Slow to start. You download and boot an entire interpreter before the first number. Rust/WASM is instantiated and running in milliseconds.
  • Still dynamically typed. You shipped the interpreter and kept the class of bugs that static types catch. You pay the WASM tax and get none of the compile-time guarantees.

Pyodide is the right answer when you must run a specific Python library client-side. For logic you own and want fast, small, and type-checked on both ends, Rust is the better trade.

// what you get

  • One source of truth. Fix a formula once; the browser and the server both get the fix. No drift between two implementations.
  • Native test, ship as WASM. Every compute crate has ordinary cargo test unit tests. The WASM build is just the same code compiled for a different target — if it passes native, it's correct in the browser.
  • Instant + offline frontend. Drag a slider, recompute locally, no round-trip, no per-call server cost.
  • Trustworthy backend. The same logic runs server-side for validation, batch jobs, and PDF reports — you never have to trust a browser's number.
  • Language-neutral edge. JSON in, JSON out. Any host language can drive it.

// what you give up

It isn't free.

  • A build step. wasm-pack sits between your Rust and your bundle. It's one command, but it's one more thing than "just import a .ts file."
  • The serialization seam. JSON across the boundary costs a parse/serialize on every call. Fine for one calculation per interaction; you'd think twice before crossing it a million times in a hot loop.
  • Rust is the floor. The team has to be able to read Rust. With an LLM doing most of the typing that floor is lower than it was two years ago — but it's still a floor.

// when this pattern fits

Anywhere you have non-trivial logic that must run in the browser and on the server and must agree: engineering calculations, pricing and tax, validation rules, simulations, parsers, geometry, anything where "the two copies disagree" is a real bug. Write it once in Rust, keep it pure, wrap a thin JSON facade, compile twice. The hard part — the algorithm — exists in exactly one place, and a machine that can't be talked out of its objections checks it every time you build.

This is the engine behind Terraforge →. For the map side of the same project, see map tiles without a tile server →

Note: this article was written by an AI. The architecture is real and shipping in production at Terraforge — the prose isn't hand-written.