Jumpstart started as a straightforward idea: describe an application as a CSV of tables and columns, and generate the rest — database, backend, frontend, tests — from that single metadata file. The first working version of that pipeline generated C#. It made sense: the generator itself is a .NET tool, and .NET’s reflection made a lot of the hard problems (mapping columns to properties, intercepting every method call for logging and authorization) almost free.
The interesting engineering happened later, when Jumpstart stopped being a “.NET code generator” and became a generator with two fully interchangeable backends — .NET and Rust — and two interchangeable frontends — Blazor and React/Node.js. Any frontend now works against any backend, because both sides of each pair implement the exact same REST contract, the same real-time protocol, and the same security model. That constraint — peers, not a primary stack with a second-class port bolted on — is what made the Rust and Node.js work hard, and worth writing about.
The problem with “just port it”
A naive port of a reflection-heavy C# codebase to Rust breaks almost immediately, because the tricks that make the .NET side compact don’t exist in Rust: no reflection, no runtime code generation, no class inheritance to hang virtual methods off of. Two specific mechanisms had to be redesigned rather than translated.
The first is the domain object model. In .NET, a Customer is a plain class with properties, and ColumnInfoAttribute reflection lets the persistence layer figure out at runtime how to map a name column to a Name property. Rust has no equivalent, so Jumpstart’s domain crate keeps every generated struct wrapping a dictionary — a BaseObject holding a HashMap<String, serde_json::Value> — with typed getters and setters layered on top that read and write that map. A Customer struct is a thin, generated view over the same dictionary the persistence and API layers see. Because the underlying data is already a JSON-shaped map, objects serialize for free, cross the REST boundary unchanged, and hand off cleanly to scripts as plain object maps. The reflection Rust doesn’t have is replaced by a generated columns() method per type, returning the metadata the SQL builders and JSON serializer need.
The second is interception. The .NET application server wraps every logic call in a DispatchProxy that reflectively intercepts method calls to apply logging, authorization, and event hooks before the real operation runs — none of that exists in Rust. Jumpstart’s Rust logic crate replaces it with an explicit dispatch model: a LogicContext carrying the record and arguments, a generated dispatch function that does the string-to-method routing reflection would have given away for free, and a single chokepoint (LogicExec::call) that runs the before-hooks, dispatches, and runs the after-hooks. The security property that matters here is that this chokepoint can’t be bypassed by accident: the raw operations (get, insert, and so on) are pub(crate), and reaching dispatch at all requires an ExecProof token whose constructor is private to the logic_exec module. Application code, API handlers, and scripts all go through <Obj>Logic::exec(...), which is the only thing that can mint that token. The one intentional bypass, exec_unchecked, is named specifically so a security review can spot it.
Everything downstream follows from those two decisions. The API layer, rather than generating one controller per object the way the .NET side does, is a single generic router that parses a request into (object, method, context) and forwards it to the dispatch model. Custom endpoints and custom logic live in hand-editable usr/ files that get spliced into the same module as the generated code via Rust’s include!, mirroring the partial class trick .NET uses for the same purpose. Even scripting got a from-scratch redesign rather than a port: the .NET backend compiles and runs C#, PowerShell, and Python scripts stored in the database; Rust has no equivalent embeddable runtimes for those languages, so the Rust backend runs Rhai instead — a small, sandboxed, Rust-native scripting language with no ambient I/O. A script can log, make an HTTP call, and read or write the database, but every database call is routed back through <Obj>Logic::exec, so a script has exactly the same authorization and audit trail as compiled application code.
The frontend followed the same peer-not-port logic
The web side of the Rust work — React, TypeScript, and Vite in place of Blazor WebAssembly — turned out to need its own iteration. It’s easy to miss in a docs page that just says “pick web-blazor or web-nodejs,” but the git history shows this wasn’t a straight line: an earlier attempt at a JavaScript frontend was pulled back out of the project before the current React implementation replaced it for good. That’s a fairly normal shape for this kind of work — the second attempt benefited from a REST and SSE contract that had, by then, been proven out against a real second backend, so the frontend had a stable target to generate against instead of a moving one.
The result generates the same two pages per domain object (a list page and an edit page), the same shared chrome (layout, navigation, data table, tab control for child records), and the same live-update behavior, just expressed as .tsx instead of .razor. Real-time updates are the clearest example of the “peers” constraint paying off: both frontends open an EventSource against GET /api/Notification/stream and filter PropertyUpdated events by domain object name — a React client and a Blazor client watching the same workflow list see status transitions land at the same time, from the same server, because the protocol was designed once and implemented twice.
What you actually pick, and what stays fixed
In practice, generating an application means choosing one entry from each of four independent axes — database, backend, frontend, and the matching test/tools set — in a config file:
{
"modelpath": "~/projects/myapp/myapp.csv",
"templatedefs": ["database-pgsql", "server-rust", "web-nodejs", "test-rust", "tools-rust"]
}
Swap server-rust for server-dotnet and web-nodejs for web-blazor, and the same CSV produces a different stack implementing identical behavior. Underneath that choice, the things that don’t move are the metadata format, the REST contract, the RBAC model (operation → op_role → op_role_member), the row-versioned audit design, and the gen/ vs usr/ split that makes it safe to re-run the generator at any time — generated code is always overwritten, hand-written code never is, in both C# partial classes and Rust’s include!-spliced modules.
Why bother with two of everything
The obvious question is whether this is worth the maintenance cost of keeping two backends and two frontends in lockstep. The case for it shows up at the edges of the stack rather than in the everyday CRUD path: Rust’s static linking produces a self-contained binary with no runtime to install, which matters for constrained deployment targets in a way a .NET runtime dependency doesn’t; Rhai’s sandboxing model — bounded operation count, call depth, and no I/O beyond an explicit capability list — makes it comfortable to execute user-authored scripts in-process in a way that’s harder to reason about with a general-purpose scripting host. Neither of those is a reason to abandon the .NET/Blazor path, which still has the more mature tooling story (SQL Server support, for one — the Rust side’s SQL Server provider doesn’t open connections yet). The point of building both is that the choice is now a deployment decision made per project, not an architectural commitment made once at the start and lived with forever.
How it actually got built
Worth saying plainly: this port was done with heavy use of Claude, working alongside me part-time over the course of about a month. Redesigning the object model and the interception pipeline for a language with no reflection isn’t mechanical translation — it required working through the .NET design’s intent (what the DispatchProxy was actually protecting, why the audited persistence strategy shapes writes the way it does) and finding a Rust idiom that preserved the same guarantees, not just similar-looking code. That a month of part-time work landed on a second backend and frontend that are genuinely at REST/SSE parity with the original — not a stripped-down demo — says as much about where AI-assisted development is useful today as it does about Jumpstart. It’s a tool that compressed the timeline; it didn’t replace the design decisions.
What’s not done yet
The honest state of things: this is a young port, not a battle-tested one. The Rust and Node.js targets pass their generated test harnesses and have run against real applications, but they haven’t had the mileage the .NET/Blazor stack has, and more testing is needed before I’d call either side production-hardened in the way the original is.
One gap is specific and known rather than vague: pre- and post-event triggers. The .NET logic pipeline fires core.event_service scripts before and after an operation; the Rust dispatch model has the hook plumbing in place and threads the right context through, but the event hook itself isn’t wired up yet — it currently sits unconnected because of a dependency-ordering issue between the logic and script crates (wiring it directly would create a cycle). The plan is to close that gap with a small callback-registry indirection, but until that lands, event-driven scripting is a .NET-only feature. If your application design leans on triggers, that’s the thing to check before picking server-rust today.