Skip to content

Composing systems

Declare the response. Interrogate the outcome.

Where τjs sits

τjs is the runtime between an HTTP request and your renderer. A route states its data, policy and rendering once; Fastify serves it, React, Vue, Solid or plain HTML renders it, and every development request leaves a record you can interrogate. Change your system without guessing.

Introspection security: development only, by construction. No production collectors.

Fig. 1 - a τjs system, front elevation, exploded A production stack drawn bottom to top in continuous line: a Fastify host carries two τjs application scopes; above app A sits the declared route contract (render, data, policy) coupled to its services, and above that the renderer that produces the response. To the left, in chain line, the build sits low beside the application scopes and its outputs rise: the structure-only graph.json it emits sits above it, and its bundles climb to enter the renderer. To the right, in dashed line, the development substrate written under node_modules/.taujs/boots and the taujs-mcp adapter that reads it: remove every dashed part and what remains is production. fastify host · http dispatch 1 τjs app a renderer root app b same host 2 declared route render data policy 3 services critical data deferred data 4 renderer react · vue solid · html 5 request response build · vite module graph bundles bundles emits 6 dist/.taujs/ graph.json structure only 7 request episode request graph episodes logs annex observed edges dev boot node_modules/ .taujs/boots/ <bootId>/ dev-only collectors 8 reads taujs-mcp stdio · fs only 11 tools · dev only tools · agents 9
  • runs in production
  • development only - no production collectors
  • written at build
  1. 1Fastify hostowns HTTP dispatch and the host lifecycle, then hands a selected τjs route over.
  2. 2τjs application scopeone per app, each with its own renderer root; several can live in one host.
  3. 3Declared route contractrender · data · policy, stated once per route.
  4. 4Servicescritical data before first byte, deferred data streamed after.
  5. 5Renderer and the responseReact · Vue · Solid · HTML produce the declared response.
  6. 6Buildτjs's build drives Vite: module graph and bundles for the renderer; emits the structure-only graph.
  7. 7dist/.taujs/graph.jsonthe structure-only request graph, written at build; read by taujs-mcp only when no dev boot is live.
  8. 8Development substratenode_modules/.taujs/boots/<bootId>/: graph · episodes · logs annex · observed edges.
  9. 9taujs-mcpstdio adapter reading the boot for tools and agents; development only.

You are about to change the catalog service. What depends on it? In most stacks that means grepping and hoping. Here you interrogate the system, and the answer is read from files the dev server already emits - never guessed from source.

Interrogate What depends on the catalog service?
taujs_who_calls_service service: "catalog"

edges

  • route /product/:id method getProduct source declared
  • route /product/:id method getProductHead source declared
  • route /product/:id method getProduct source observed
  • route /compare/:id method getProduct source observed
  • route /product/:id method getProductHead source observed

hostObserved

  • route /api/products/:id method getProduct source hostObserved

Declared means intended. Observed means exercised. Missing means not seen yet - not "safe".

What the tool says about these labels

declared = from config (a serviceData edge, a deferred entry or a head edge); observed = seen in dev traffic through a τjs page route, never complete truth; hostObserved = seen in dev traffic through a Fastify route the application registered itself, reported separately so it is never mistaken for a declared edge. methodCallCount is the method-wide total for the boot; routeCallCount is that route’s own attribution. lastObservedAt is also method-wide: the last time any route called this method, not the last time this route did.

The rows come from two artefacts under the application’s own node_modules/.taujs/boots/<bootId>/: the request graph records what taujs.config.ts declares; observed edges record what real development requests did. An agent working on a τjs application is handed the substrate’s own description of the application it is modifying, over MCP. When there is no live boot, the episode tools refuse rather than answer stale; structural answers from an old build are labelled as such.

How the tools answer, and what they refuse.

Each declared route states its data, policy and rendering once. That declaration is what Fastify dispatches and what the renderer consumes - and it is the same thing the graph reports. The contract is operational, not descriptive: data loading, rendering, policy, tracing and tooling all consume the same declaration, so the answer a tool reads cannot drift from the code the server runs.

Episodes add the second kind of truth. Every development request leaves a record of what actually happened: which services were called, when, with what outcome. Declared is intent; observed is behaviour; the page shows you both, labelled, and never substitutes one for the other.

Traces alone are available on any framework. What is unusual here is where the substrate sits - at the HTTP request and response boundary, with the renderer still yours - and that the boundary is inspectable by construction rather than by instrumentation added later.

Request contracts and data ownership.

A request matches a declared route. Critical data resolves before the response commits, where a failure can still become an HTTP error. Deferred data starts with the request, never delays independent shell content and cannot decide the status. The renderer - the application’s choice - turns the result into HTML, server-rendered or streamed; values consumed during rendering hydrate without a client refetch. Then the episode is written.

Here is a product page that streams, with its reviews deferred alongside:

export default defineConfig({
apps: [{
appId: "storefront",
entryPoint: "",
renderer: reactRenderer({ project: "./tsconfig.json" }),
routes: [{
path: "/products/:id",
attr: {
render: "streaming",
data: serviceData("catalog", "getProduct", (p) => ({ id: String(p.id) })),
deferred: { reviews: serviceData("reviews", "forProduct", (p) => ({ id: String(p.id) })) },
},
}],
}],
});

The episode that request leaves is the second proof. Its marks are the contract’s own stages, so the time a request spent has a location, not a guess:

Interrogate Where did this request spend its time?

/product/42 modestreaming outcomecomplete status200

startMs
0 ms
matched
0.1 ms
devAssetsReady
2.4 ms
dataStart
4.8 ms
dataEnd
5.8 ms
head
7.3 ms
shellReady
7.7 ms
allReady
7.9 ms
catalog.getProductHead
3.2–3.3 ms · ok
catalog.getProduct
5.6–5.8 ms · ok

Each declared page is a real Fastify route, and a route can also declare head metadata, authentication and Content Security Policy. Everything after hydration - mutations, polling, subscriptions, UI-local fetching - stays in your application.

τjs composes existing tools rather than replacing their native APIs. Fastify owns HTTP routing, plugins, hooks, decorators and the host lifecycle. Vite owns the module graph, development pipeline and production bundles. React, Vue, Solid or plain HTML keep their own rendering, streaming semantics and client runtimes.

There is no second HTTP router or parallel plugin system. Supply your own Fastify instance and τjs operates inside an encapsulated scope; omit it and τjs creates the host. You do not convert an application: start with what you have and declare the routes whose initial responses benefit from server coordination. Policy, data orchestration and service access stay where they are when the renderer changes.

Adopt the model incrementally.

One architecture, several application shapes

Section titled “One architecture, several application shapes”

One Fastify host can serve a storefront, an account area and an operations console - each a separate application with its own renderer, module graph and output bundle, each free to choose a different one. Every URL belongs to exactly one application; undeclared URLs remain client-rendered by omission. Applications compose through routes and assembled build output, not runtime module federation, and the whole system answers to the same graph and the same episodes.

How multi-app composition works.

Introspection security: development only, by construction

Section titled “Introspection security: development only, by construction”

The server you develop against is the server you deploy: the same Fastify routes, the same contract, the same orchestration. Development is production plus exactly two things - Vite’s module pipeline and the introspection collectors - and NODE_ENV decides which you are running: development is development, anything else is production. There is no third mode.

Episodes, logs and observed edges are collected by the dev server. The production runtime structurally excludes those collectors - there is nothing to switch off, no sampling to tune and no request data leaving a deployed host. Builds emit one artefact, a structure-only graph at dist/.taujs/graph.json, so structural questions can still be answered from the last build, labelled stale.

The MCP server opens no network connections and loads no configuration: the files are its credential. Several live boots refuse with multiple_active_boots; a folder whose ids disagree is refused as substrate_inconsistent. Field values in answers are your application’s data - capped, never treated as instructions.

Architecture overview.