---
title: "Protect a route"
audience: "anyone with a page only signed-in visitors should see"
description: "You'll put a page behind a sign-in, turn an anonymous visit into a redirect and read who the visitor is from a loader. The check is one file and it runs…"
snippet_caption: "app/middleware.ts"
snippet: "import type { MiddlewareCtx, MiddlewareResult } from \"@generated/client\";\n\nexport async function middleware({ request, identity }: MiddlewareCtx): Promise<MiddlewareResult> {\n  if (!request.path.startsWith(\"/account\")) return {};\n  if (identity) return {};\n  return { redirect: `/sign-in?next=${request.path}` };\n}"
---

import { Install } from "@src/ui/Install";

You'll put a page behind a sign-in, turn an anonymous visit into a redirect and read who the visitor is from a loader. The check is one file and it runs before any of your routes do.

<Install captured="0.x" />

## Session and identity

A **session** is the visitor's scratch space: a cart, a draft, a preference. Every visitor has one, signed in or not. You declare its shape in `app/schemas/session.ts` and a body reads and writes it as a plain object.

An **identity** is who the visitor is: a subject and a map of claims. A body reads `identity.subject` and `identity.claims.<name>`; it reads `null` when nobody is signed in. You never assign to it. It arrives on the session when a login finishes.

> Don't keep a `user_id` in the session yourself, since any code path could set it. Read `identity`, which only the login flow sets.

## Write the check in middleware

`app/middleware.ts` at the top of the app runs before every request that is not a static file: pages, the action route and handlers alike. What it returns decides the request.

```ts title="app/middleware.ts"
import type { MiddlewareCtx, MiddlewareResult } from "@generated/client";

export async function middleware({ request, identity }: MiddlewareCtx): Promise<MiddlewareResult> {
  if (!request.path.startsWith("/account")) return {};
  if (identity) return {};
  return { redirect: `/sign-in?next=${request.path}` };
}
```

An empty object lets the request continue. `redirect` answers with a location, `rewrite` serves another path under the same URL and `status` with a `body` answers outright.

Put the check in middleware instead of in each loader. A route you add under `/account` next month is then covered the day you add it. The action route is covered too, so a protected page's actions cannot be called by a visitor who was refused the page.

## Reading the identity in a loader

```ts title="app/routes/account/page.loader.ts"
import type { Ctx } from "@snapfire/fsr";

export async function load({ identity, services }: Ctx<"/account">) {
  return {
    name: identity?.claims.name ?? "",
    orders: await services.shopping.ordersFor({ subject: identity?.subject ?? "" }),
  };
}
```

The host carries the identity onto outbound service calls on its own, so a service that needs to know who is asking is already told. Passing the subject as an argument, as above, is for a service whose method takes it as data rather than as who the caller is.

## Prerendering

> `identity` is per request, so a route whose loader reads it renders per request. A page behind a sign-in can't be prerendered and the build won't try.

## Configure the login

Add an `[auth]` section to the configuration:

```toml title="config/app.toml"
[auth]
provider = "file"
login = "/sign-in"
```

That mounts three routes the framework owns, the way it owns the action route. `GET /auth/login` starts the flow, taking `return_to` from the query, else the `Referer`'s path, else `/`. The target is always a path on this origin. `/auth/callback` finishes it, as a form post, a JSON post or a GET carrying the provider's query. A success is a 303 to where the flow began. A refusal is a 303 back to the login page with `error=denied`. A callback with no flow in progress gets a 400. `POST /auth/logout` verifies `_csrf` and clears identity and custody.

You write the login page yourself. Auth never renders anything, so `[auth] login` names a route you wrote. `/sign-in` is an ordinary page with an ordinary form.

The `file` provider is `DevProvider` over `config/auth.toml` beside `app.toml`: a list of accounts with passwords and optional claims. It exists so the application can be written against identity from the first day and swap in the real provider later without a body changing, because a body only ever sees `identity`.

## Log out

Signing out is a form that posts to `/auth/logout` with the CSRF token. Put it on the account page, which reads `csrf_token` like any other prop:

```tsx title="app/routes/account/page.tsx"
import type { AccountProps } from "@generated/client";

export default function Account({ name, orders, csrf_token }: AccountProps & { csrf_token?: string }) {
  return (
    <section>
      <h1>Signed in as {name}</h1>
      <p>{orders.length} orders</p>
      <form method="post" action="/auth/logout">
        <input type="hidden" name="_csrf" value={csrf_token ?? ""} />
        <button type="submit">Sign out</button>
      </form>
    </section>
  );
}
```

Clearing the identity clears the tokens held beside it, since the two cells persist together. Only the login and logout routes change the identity. A body can't set or clear it.

Next up: [360. Add light and dark themes](https://snapfirers.com/fsr/learn/360-add-light-and-dark-themes).
