Known limitations
The honest list. Each of these is something that will surprise you if you hit it without warning.
Bun only
The dev server, the production server and the build all require Bun. Node does not work, and there is no compatibility layer. $app-style platform helpers are not abstracted for other runtimes because there is only one.
No virtual DOM
Components run once. After that, a change updates only the nodes whose expressions read the changed state. There is no diff, which is the point — and also the constraint: the compiler has to see which signal each expression reads, so a read hidden inside a plain helper function is a read it may not connect to the right node.
onRouteRender disables streaming
Any plugin implementing onRouteRender forces the whole response to be buffered into a single string, because rewriting finished HTML requires having all of it. Pages still render correctly, they just arrive at once, and the loading export is skipped on those routes.
Grimoire logs a warning at server start naming any plugin that does this, so the cause is visible instead of being something you notice as "streaming stopped working".
If you need to touch headers, use onRequest instead — it operates at the Response level and leaves the body alone. See plugins.
loading is first-load only
The loading shell is used for the initial document request. It is not used for client-side navigation, where the previous page stays on screen instead. A plugin using onRouteRender disables it entirely.
loading takes no data
It renders before the route's load has finished, so it receives no arguments and no data. It can only use hardcoded or module-level values. Keep it the same shape as the real page or the swap will reflow.
A route needs a page or a server handler
A directory with only a +page.server.ts is not a route. The scanner drops it and there is no error explaining why — the route is simply absent from sigil routes. If a page 404s and the file looks right, check whether it has a +page.tsx or a +server.ts.
This bites hardest on logout, consent and other POST-only endpoints: a form that posts somewhere handled by +page.server.ts only, with no +page.tsx, is unreachable.
canMatch returning false is a 404, not a fallthrough
The route has already won by the time canMatch runs, so declining it means "this page does not exist here" rather than "try the next route". For an auth gate, throw redirect("/login") is what you want. See load functions.
Actions: return versus throw
A plain object returned from an action is a success, and Grimoire redirects. To redirect deliberately, throw the sentinel. A raw Response thrown from an action is swallowed — the throw is treated as an error path, not a reply.
Cookie path defaults to /
A cookie set without an explicit path is scoped to / by default. This is almost always what you want; the surprise is the reverse case, where an explicit path narrower than the request directory means a session set at /account/login never reaches /.
Bun binds IPv6 by default
The dev server listens on [::1], not 127.0.0.1. A script that curls http://127.0.0.1:3001 gets connection refused while the page works fine in a browser. Use localhost, or set host in sigil.config.ts.
Scoped styles come from the loading shell
On a streamed page, scoped styles are collected from the loading export and hoisted before the first flush — that is what lets the skeleton paint styled. Styles that exist only in the real page's JSX are not in that first batch, so on a slow first load you can get a brief flash of unstyled content before they arrive with the second flush.
Put the shape-critical styles in the loading shell, or accept the flash on slow connections. On non-streamed pages this does not apply.
Plugin transform sees compiled JavaScript
transform runs after compilation, so its input is JS, not TSX. It is the right place for string-level work — import rewriting, comment injection — and the wrong place for anything that needs to understand the component tree.
Missing values are "", not undefined
$env/* reads a missing variable as an empty string. A missing secret therefore surfaces as a confusing runtime failure rather than a clear crash. Validate required variables in init in hooks so the process fails at boot with the name in the message.
sigil g --help omits auth
The auth generator is implemented and works, but it is missing from the help text, so the listed types are incomplete. Found in cli/src/index.ts — the addHelpText list needs auth added.
Ports must be free
sigil start fails with EADDRINUSE if the coordinator's port or a worker's port is taken, and the worker crash-loops respawning. A previous dev server still holding 3001 is the usual cause; ss -ltn | grep 3001 finds it.