middleware.ts) runs first if present.route.ts), then page routes.node:http, or Bun.serve on Bun; HTTP/2 if TLS is configured).route.ts that exports WS, and in dev the live-reload SSE stream at /__webjs/events. Both are intercepted on the node and Bun shells alike. The node shell also emits 103 Early Hints here in production, with modulepreload URLs for the matched page; Bun has no informational-response API, so that step does not exist on the Bun shell.webjs config are answered: webjs.redirects and the webjs.trailingSlash canonical-form policy, both of which reply with a redirect status and a Location header rather than routing the request onward. These are configured by your app but resolved by the framework ahead of everything below, so a 308 from a redirect rule is answered without your root middleware running. That is the case most likely to surprise you, so it is worth remembering specifically./__webjs/health, /__webjs/ready, /__webjs/version), the core runtime (/__webjs/core/*), the dev reload client and its SharedWorker (/__webjs/reload.js, /__webjs/reload-worker.js), and downloaded vendor bundles (/__webjs/vendor/*). Not everything under /__webjs/* is here: the server-action RPC endpoint is routed with the app, below. In development only, /public/* plus the /sw.js and /offline.html root remaps and /favicon.ico are served here too, so a stylesheet is never queued behind the startup analysis.middleware.ts) runs next if present, for every request not already answered above. The rule behind the exceptions is worth holding onto rather than the list: anything the listener shell or the framework's pre-analysis stage answers never reaches your middleware, and everything routed with the app does./__webjs/action/<hash>/<fn>), static files, user source modules, API routes (route.ts), then page routes. The action endpoint is routed here rather than answered early, so root middleware DOES run for a server action, which is what lets you gate actions with auth or rate limiting. In production this is also where /public/* is served, so a middleware that guards an asset still guards it.
- Step 6 above produces real HTML. The SSR pipeline runs every web component's render() on the server, so the component's initial markup is in the response before any script loads. The browser paints content, processes <a> links, and handles <form> submissions before any JavaScript runs. The client router, custom-element upgrades, and Suspense streaming are layered on top of that HTML. They enhance an already-working page, they do not constitute it.
+ The SSR pipeline above produces real HTML. It runs every web component's render() on the server, so the component's initial markup is in the response before any script loads. The browser paints content, processes <a> links, and handles <form> submissions before any JavaScript runs. The client router, custom-element upgrades, and Suspense streaming are layered on top of that HTML. They enhance an already-working page, they do not constitute it.
<a> links navigate, and display-only custom elements render correctly.Middleware works identically in backend-only mode. Place middleware.ts files at the root or in any segment directory:
Middleware in WebJs lets you intercept requests before they reach your pages, API routes, or server actions. Use it for authentication, logging, rate limiting, CORS, header injection, or any cross-cutting concern. WebJs supports two levels of middleware: a single root middleware and per-segment middleware scoped to subtrees of your route hierarchy.
Place a middleware.ts at the root of your project (next to app/, not inside it). This middleware runs on every request before WebJs routes it to a page, API route, or server action. Any of middleware.ts, .js, .mts, or .mjs works, and .ts wins if you somehow have more than one.
Place a middleware.ts at the root of your project (next to app/, not inside it). This middleware runs on every app request before WebJs routes it to a page, API route, or server action. Any of middleware.ts, .js, .mts, or .mjs works, and .ts wins if you somehow have more than one.
Some requests are answered before it and so never reach it. The rule is worth holding onto rather than a list: anything the listener shell or the framework's pre-analysis stage answers bypasses root middleware, and everything routed with your app reaches it. In practice that means WebSocket upgrades bound for a route.ts exporting WS, the dev live-reload SSE stream at /__webjs/events, and the framework's own /__webjs/* runtime assets and health probes, which your app needs in order to boot at all. In development only, static files under /public/* (plus the /sw.js and /offline.html root remaps and /favicon.ico) are answered there too, so a stylesheet is never queued behind the dev server's startup analysis.
One case is worth calling out on its own, because the rule gives the right answer and intuition does not: webjs.redirects and webjs.trailingSlash are configured by you, but they are resolved by the framework before your middleware runs. So a request that a redirect rule answers with a 308 never reaches root middleware, and a logging or auth middleware will not see it. If you need middleware to observe those requests, do the redirect in the middleware rather than in config.
Two things that look like exceptions are not. In production those /public/* files go through root middleware normally, so a middleware that protects an asset still protects it where it counts. And server actions are routed with your app, not answered early, so root middleware DOES run for an action call: that is what lets you gate actions with auth or rate limiting.
Place a middleware.ts inside any directory under app/ to scope it to that subtree. It runs only for requests whose URL matches that segment and its children.