Understanding the proxy boundary
Every full-stack app has a seam down the middle: C# on one side, TypeScript on the other, HTTP in between. Normally you are the glue across that seam — you write a controller, hand-write a DTO, redeclare its shape in TypeScript, wrap it in a fetch, and then spend the rest of the project keeping both sides in agreement. Nothing tells you when they drift: a renamed field compiles happily on each side and fails in the browser at runtime.
Arc removes that seam. You write the command or query once, in C#, and the build generates the TypeScript your frontend calls. There’s no second declaration to keep in sync, because there’s no second declaration at all. This page is about why that works and what keeps it honest — the idea at the center of everything Cratis calls “full-stack type safety.”
One source of truth
Section titled “One source of truth”The thing to internalize: your C# command and query records are the contract. The TypeScript proxy isn’t a parallel definition you maintain — it’s a projection of the C# types, regenerated on every build. There is exactly one place the shape of RegisterAuthor is defined, and it’s the C# record.
Because the proxy is generated from the very types Arc uses to bind the incoming HTTP request, the client and the server can’t disagree about the wire format. The compiler — on both sides — is enforcing one contract.
Walk the boundary
Section titled “Walk the boundary”-
Write the slice in C#. A command is a record; the behavior lives in
Handle()on the record. This is the only place the shape is declared:[Command]public record RegisterAuthor(AuthorId Id, AuthorName Name){public AuthorRegistered Handle() => new(Name);} -
Build.
dotnet buildruns the Arc proxy generator — a Roslyn source generator wired in as an MSBuild step. It reads your compiled assembly, finds every[Command], every[ReadModel]query method, and the types and enums they touch, and writes a typed TypeScript proxy for each — mirroring your namespace folders, with anindex.tsper folder. -
Import the proxy in React. It’s already there, generated from the C# above — call it as if it were local, typed code:
import { RegisterAuthor } from './Authors/RegisterAuthor'; // generated<CommandDialog command={RegisterAuthor} title="Add author"><InputTextField<RegisterAuthor> value={i => i.name} title="Name" /></CommandDialog>
What keeps it honest
Section titled “What keeps it honest”Here’s the payoff — and the whole reason the boundary is generated rather than written. Suppose you rename Name to FullName on the command:
[Command]public record RegisterAuthor(AuthorId Id, AuthorName FullName) // was Name{ public AuthorRegistered Handle() => new(FullName);}// After `dotnet build`, the generated proxy has `fullName`, not `name`.// This line stops compiling until you fix it — caught at build, not in production:<InputTextField<RegisterAuthor> value={i => i.name} title="Name" />// ^^^^ Property 'name' does not exist on RegisterAuthorYou didn’t run the app. You didn’t open the browser. The TypeScript compiler caught the drift the instant the contract changed, because the accessor i => i.name points at a property the regenerated type no longer has. That’s the difference between a generated boundary and a hand-written one: drift is a compile error, not a production incident.
What gets generated
Section titled “What gets generated”Each build, Arc emits typed proxies for:
- Commands — a class you instantiate and execute (or hand to a
CommandDialog), with every parameter — route, query string, body — flattened into typed properties. - Queries — a proxy exposing a React
.use()hook that returns the typed result with its loading and error state; observable queries return a live subscription that re-renders on change, with noperformto call and no polling. - Types and enums — every complex type and enum your commands and queries reference, so nested shapes are typed too.
- Identity details — your custom identity type, so the signed-in user is typed on the frontend.
Each has a detailed reference: Commands, Queries, and Identity details. The frontend proxy-generation guide shows the generated TypeScript for each, with the .use() and useSuspense() hooks.
It’s a build step, not a checkout
Section titled “It’s a build step, not a checkout”A few things follow from the proxies being generated, and they’re worth holding onto:
- You never edit a generated file. Treat the generated folder like build output — change the C#, rebuild, and the TypeScript follows.
- The generator tracks what it made and removes files for commands or queries you delete, so the generated tree doesn’t accumulate ghosts. (See File index tracking.)
- The sequencing is fixed: backend compiles → proxies generate → the frontend can reference them. For a single slice you can’t build the React half before the C# half compiles — which is exactly why the workflow is backend-first.
Why this is the thing that matters
Section titled “Why this is the thing that matters”Plenty of frameworks give you CQRS, or an event store, or a component library. The proxy boundary is what makes Cratis full-stack: the type safety doesn’t stop at the edge of the backend — it runs all the way to the React component. It’s why a Cratis feature is one thing expressed in two languages, kept in agreement by the build instead of by discipline.
Ready to see it run? Build your first command and query, then wire it into React — and watch the same property name flow from C# all the way to the screen.