Guards
A guard decides whether a page route may be served. It runs on the server, before the page is rendered, so it's a real authorization boundary. A guard can read the request, check the session, and then allow the route, redirect, or deny it. This is how you keep an admin area, a members' section, or any protected page from loading for the wrong person.
Defining a guard
Add a _guard.ts to a route directory. It exports a named guard function that receives a context object with the matched route's params and the incoming request:
// src/routes/dashboard/_guard.ts
export function guard({ params, request }) {
// params: route params for the matched path ({ id: "42" }, or {} for a static route)
// request: the incoming Request; read cookies/headers, or hand it to an auth service
// ...decide what to do
}The guard's return value decides the outcome:
| Return | Outcome |
|---|---|
true, undefined, or nothing |
Allow. Continue to the next guard, then serve the route. |
false |
Deny. Respond 403. |
| a string | Redirect. Respond 302 to that path. |
| throws | Deny. Respond 403, failing closed. The framework logs the error. |
Guards can be async.
The common case: require a logged-in user
Because a guard runs on the server inside a request scope, it can inject() your services and read the session off the request, with the same inject() you use everywhere else:
// src/routes/dashboard/_guard.ts
import { inject } from "@kyln/core";
import { AuthService } from "../../services/auth.service";
export async function guard({ request }) {
const user = inject(AuthService).getAuthenticatedUser(request);
return user ? true : "/login";
}An anonymous visitor to /dashboard (or anything beneath it) gets redirected to /login; an authenticated one is let through. Because this runs on the server before the page renders, the protected page's markup never reaches a visitor who shouldn't see it, whether the request is a normal navigation, a hard refresh, a curl, or a crawler hit.
Scope: a guard covers its subtree
A guard protects its own directory and everything beneath it, the same way a layout wraps everything beneath it. A guard at src/routes/admin/_guard.ts protects /admin, /admin/settings, and /admin/reports/monthly alike. You guard the section once, at its root.
Guards nest and compose parent-first: for a deeply nested route, the outer guard runs first, and the first guard to deny or redirect wins (an inner guard doesn't run once an outer one has blocked the request). Every spelling of a guarded path is covered: trailing slashes, /index.html, encoded slashes, and .-segments all normalize to the same protected route, so a guard can't be slipped past with a URL variant.
Guards run only on the server
Guards never run in the browser. That's deliberate: it means a guard can safely use server-only things (a database, bun:sqlite, an auth service that reads httpOnly cookies) without any of that leaking to the client. When the client navigates to a guarded route, it hands the navigation to the server, which enforces the guard; you don't write anything special for that to happen.
Two consequences worth knowing:
- Guarded routes are never prerendered. A prerendered page is static HTML and can't run a guard, so Kyln skips prerendering for any route under a guard. The guard always gets to run.
- Denials are
403, not404. Hiding a route's existence isn't meaningful here (the client's route table already ships to the browser), so a denied route answers honestly with403. When you'd rather not reveal a protected area at all, return a redirect instead of denying.
Guards protect pages; API routes guard themselves
A _guard.ts protects page routes, the pages a browser navigates to. API endpoints under src/api/ are not covered by page guards; an endpoint authorizes each request in its own handler, which is the pattern shown in API endpoints (inject an auth service, check the request, return 401 when it fails). Between the two, page navigation and API calls are each guarded at their own layer.
Next
With routing, rendering, data, and access covered, Deployment takes your app to production.