Application Architecture

How a Roost application is structured, what happens at boot time, and how a request travels from the Cloudflare edge to a response.

A Single Entry Point at the Edge

Every Roost application is a Cloudflare Worker — a V8 isolate that receives HTTP requests and returns HTTP responses. There is no load balancer to configure, no Nginx to tune, and no server process to keep alive. The Worker is both the web server and the application framework running inside it.

The Application class from @roostjs/core is the heart of this setup. It holds the service container, owns the list of registered service providers, and exposes a single handle(request) method that the Worker's fetch handler calls for every incoming request. All of Roost's bootstrap logic flows through this one object.

Boot Sequence

Roost boots lazily. When the first request arrives, Application.handle() notices the application has not yet booted and calls Application.boot() automatically. Boot runs in two strict phases. First, every registered service provider's register() method is called in order — this is where bindings are added to the container. Then, every provider's optional boot() method is called. The separation matters: a provider's boot() can safely resolve things registered by a different provider, because all registrations are finished before any booting begins.

Once booted, the application stays booted for the lifetime of the V8 isolate. On Cloudflare Workers, isolates are reused across many requests, so the boot cost is paid once and amortized. Subsequent requests skip straight to the middleware pipeline.

The Middleware Pipeline

After boot, each request enters the middleware pipeline. Roost builds a new scoped container for every request — a child container that inherits all singletons from the application container but can register its own request-scoped bindings without polluting the shared state. The pipeline hands this scoped container to each middleware in sequence.

Middleware is a chain of responsibility: each piece calls next(request) to pass control to the next middleware in the chain, or returns a Response early to short-circuit. This lets authentication middleware reject requests before they reach route handlers, and response middleware modify the response on the way back out. The final handler in the chain — called the "destination" — is where routing logic lives.

TanStack Start and the Context Bridge

When Roost is used with @roostjs/start, TanStack Start handles routing and SSR. The Roost application runs as middleware inside TanStack Start's request pipeline, not as a standalone server. This means TanStack Start's file-based router resolves the route, but Roost's middleware pipeline runs first — authenticating the request, resolving the organization, and making services available via the scoped container.

Server functions (via roostFn) capture the request-scoped container and make it available to TanStack Start's data-loading layer. This bridges the server-side Roost context into the React component tree without passing dependencies through props or relying on module-level globals.

Further Reading