Every form in the console validates against a schema no one wrote on the client. A route declares its shape once, and the operation id on that route b...
|
Jan 14, 2026Giant Context builds and hosts websites. The website is the product: public, indexed, served fast to strangers. The console is the portal where a customer builds and manages theirs, an authenticated app behind a login. Two surfaces, opposite jobs.
The console is not a Next.js app, and it is not react-router or TanStack Router either. Routing there is a system I own outright. There was never a bake-off, because by the time the console needed routing the codebase already generated its API client, its cache keys, its hooks and its form validation from one schema. The route table was one more output from machinery that was already running.
The architecture committed to its structure before there was a product in the repository. The API is the hub, and everything that renders is a satellite. The API describes itself in an OpenAPI schema, and every consumer works from that description: the browser SPA that is the console, the Electron desktop shell, a native mobile client if one ever becomes necessary, the SDKs, and consumers that are not browsers at all. Tool-calling agents are already in the repository. None of them get a private contract. The schema is the contract.
That last group is the reason this matters more than it looks. I want an SDK, and I want an SDK because I want the platform reachable over MCP, so an agent can operate it the way a person operates the console. A model calling this API should not have to know a rendering framework exists. If the API is tangled into one, then every non-browser consumer either drags that framework along or reimplements the contract by hand, and the second one is how contracts drift.
Satellites are replaceable on purpose. Electron is a solid way to wrap an SPA into a desktop app, and I do not fully trust it as a long-term answer, because wrapper quality is somebody else's roadmap. If the day comes when only a real native app will do, the satellite gets swapped and the hub does not move. The presentation layer stays decoupled from the schema, so that no rendering choice made today can hold the data model hostage later.
The same shape pays elsewhere. A self-describing API gives you a Swagger surface, generated client bindings, and one place where permission checks live against the schema instead of once per rendering framework.
Next fights this shape, because Next is not a router. It is a commitment to a rendering server. Your routes become its route files, your data fetching runs inside its request lifecycle, and your components are split into ones that may run on the server and ones that may not. Those are reasonable rules for a rendering server to have. But adopting them puts the framework at the centre of a system whose centre is supposed to be the schema.
Next earns its keep when server rendering is the product: pages that must index, first paint for anonymous cold visits, markup assembled before the client wakes up. That is a precise description of the websites this platform hosts, the thing customers come here to make. They exist to be found and to load fast for strangers, so they render through Next, with incremental static regeneration and a sixty-second revalidation window.
The console is the opposite case. Behind a login, nothing to show a crawler, long warm sessions where a served HTML shell buys nothing. Same repository, same day, two rendering decisions that point in opposite directions because the two surfaces are answering different questions.
The objection to a homegrown router was never the runtime. Path matching is a solved problem the size of a util file. The objection is everything around it: keeping the route table, the view files and the types in agreement, forever, by hand. Somebody has to notice when a new view file appears and add it to the table. Somebody has to notice when a folder is renamed and fix the path. That work never ends and nobody is assigned to it, which is why using a library is the right default for almost everyone.
Here that work is already automated, for reasons that had nothing to do with routing. The generated package runs a set of generator scripts:
packages/generated — the generator scripts
"generate:api": "openapi-zod-client ${OPENAPI_SPEC_FILE:-http://127.0.0.1:5175/docs/json} -o src/client/api.ts","generate:keys": "tsx scripts/generate-keys.ts","generate:hooks": "tsx scripts/generate-hooks.ts","generate:client-routes": "tsx scripts/generate-client-routes.ts","generate:mocks": "tsx scripts/generate-mocks.ts","generate:factories": "tsx scripts/generate-factories.ts",In the first line, the client's API bindings materialize from the running dev server's own OpenAPI spec. That 127.0.0.1:5175 URL is the API describing itself, and the description becomes typed code. The same machinery emits the query cache keys, the data hooks, the form validation, the mocks and factories the tests run on, and the same habit keeps the reactive layer honest.
All of that was built to serve the API client. Routing was added to a pipeline that already existed, which is the entire reason it was cheap. And the input it needed was already sitting there, because a folder tree is a declaration that nobody has to maintain separately.
A directory means a path. A bracketed directory means a parameter. A view file means a page. The generator walks the tree and emits the route table, and the build refuses to proceed without regenerating first, so a folder added without a regeneration fails typecheck rather than 404ing in a browser.
The generated route table
// AUTO-GENERATED - DO NOT EDIT// Run "pnpm generate:client-routes" to regenerate
import { default as OrganizationOrganizationSlugProjectsProjectSlugBrandingBrandingIdPage } from "@giantcontext/core/routes/organization/[organizationSlug]/projects/[projectSlug]/branding/[brandingId]/view";
export const routes: RouteType[] = [ { path: "/organization/:organizationSlug/projects/:projectSlug/branding/:brandingId", component: OrganizationOrganizationSlugProjectsProjectSlugBrandingBrandingIdPage, params: ["brandingId"], },];That import identifier is a name nobody would type, and nobody has to. The hooks that fetch a route's data, the validation on its forms, the cache keys it invalidates, all of those are generated from the same schema, so a view file receives them rather than importing them by hand. The only thing anyone writes is the folder name.
Which means that identifier is doing real work. It is the join between a folder in the console and an operation in the API, made by machine at both ends. Rename the folder and the path changes with it. Rename the operation and the client method changes with it. Neither one can be quietly half-done.
The runtime that remains matches a path, mounts the view, and hands over the params. It is one Router component and its utils, small enough to read in a sitting.
I never evaluated react-router or TanStack Router. No comparison, no matrix, no afternoon with their docs.
Either one would have replaced the cheap part, path matching, and left the expensive part alone. The agreement between the folder tree, the route table and the types would still have been mine to maintain by hand. That is the wrong half to outsource, and it stays the wrong half whichever library is on the other side.
Next got the most valuable idea in front-end routing right, and it is not server rendering. It is that a folder tree can be the route table. Put a file in a directory and the URL exists. That part I took.
What I did not take was the rendering server that comes with it. The OpenAPI schema drives the client, the validation, the cache and the tests, and it has to keep driving them for surfaces that are not a browser at all: an SDK, an MCP server, a desktop shell, a native app if one is ever needed. Put a rendering server at the centre and every one of those has to either adopt the framework or restate the contract by hand.
So Giant Context keeps the schema at the centre and treats rendering as a choice made per surface. The console reads its folder tree the way Next does and gets everything else from the generator. The hosted websites render through Next, because for them server rendering is the product. A future SDK or MCP server will read the same schema and never know either decision was made.
I rolled my own Next because by the time the console needed routing, the route table was the only piece missing. The client, the hooks, the cache keys and the validation were already being generated, and the plan is to generate all of it.
Remote, Pacific time, full-time or contract. Get in touch.