packages/router/src/middleware.ts implements a full middleware system, including an
explicit client mode:
defineMiddleware(handler, { mode: 'client' }) // middleware.ts:35-43
… // honoured at :302-303The client router never calls it:
$ grep -c runMiddleware packages/router/src/client.ts
0The only callers are build/server-side — packages/stx/src/ssg.ts:1017 and
dev-server/serve-app.ts:714. So mode: 'client' middleware is registered, typed,
documented and never executed. A SPA navigation runs no guard at all.
definePageMeta compounds it: PageMeta.middleware?: string | string[]
(packages/stx/src/head.ts:113) and PageMeta.validate? (:118) are both declared, and
neither is read anywhere — the only .middleware consumers are the unrelated config-level
middleware in config.ts:890 and caching.ts:180.
What that costs an app
Every auth-gated page hand-writes its own guard as a bare pre-paint <script>, because
there is nowhere else to put it. From a real app, repeated in four page bodies:
if (!/(^|;)\s*bughq_token=/.test(document.cookie)) {
window.location.replace('/login')
return
}That is a raw cookie regex, a full page load instead of a SPA navigation, and no shared place to change the rule. It is also the reason a "no vanilla JS" cleanup could not finish: the guard cannot be a directive or a composable, because it must run before hydration and before the runtime exists.
What peers provide
| framework | API |
|---|---|
| Vue Router | router.beforeEach, per-route beforeEnter |
| Nuxt | definePageMeta({ middleware: 'auth' }), defineNuxtRouteMiddleware |
| Angular | CanActivate guards |
| Laravel | route middleware groups |
| Remix | loader redirects |
Every one of them makes "redirect unauthenticated users away from this route" a declaration. stx has the pieces for it already — this is wiring, not design.
Suggested direction
Two changes, and the first is small:
- Call
runMiddlewarefrom the client navigation path.client.tsalready has the destination URL and the response headers; the middleware context type (createMiddlewareContext) exists. Filter tomode: 'client'(or'all') and honour a redirect result by handing off tonavigate(to, { replace: true }). - Read
PageMeta.middlewareandPageMeta.validate. They are the documented surface —definePageMeta({ middleware: 'auth' })is what a Nuxt-shaped user will write first, and today it silently does nothing, which is worse than not offering it.
Worth deciding explicitly what happens on a cold load of a guarded route, since the
client guard cannot help there: either the server middleware covers it (it already runs at
serve-app.ts:714) or the framework should say the guard is SPA-only so apps know they
still need a server check. Right now an app that adopts client middleware and drops its
inline script would be protected on navigation and unprotected on refresh, which is the
worst of the three outcomes.