# 201. Actions and the session

**For:** app developers. Changing data and knowing who asked.

## Actions

A loader reads data and an action writes it. An action lives in `actions.ts` beside the page. It is declared with `action` instead of just exported, so the build knows its input type and can put it in the contract.

```ts
import { action, fail } from "@snapfire/fsr";

export const pay = action<{ amount: number }>(async ({ input, identity, services }) => {
  if (!identity) {
    fail("unauthorized", "sign in to pay");
  }
  return services.ledger.pay({ invoice: input.invoice, amount: input.amount });
});
```

The browser calls it through a generated, typed handle:

```tsx
import { actions } from "@generated/client";

await actions.invoice.pay({ amount: 1200 });
```

There is no URL to build and no body to serialise. The build knows the id, the input type and the return type. A call that does not typecheck does not compile.

## Guards

A guard that reads nothing external is evaluated before any service socket is opened, so an invalid call never leaves the server. `fail(kind, message)` is a statement, never an expression. Its kind maps onto a status:

| Kind | Status |
|---|---|
| `unauthorized` | 401 |
| `not_found` | 404 |
| `invalid` | 400 |
| `conflict` | 409 |
| `timeout` | 408 |
| `unavailable` | 503 |
| `internal` | 500 |

The kind is a string literal, because the build matches on it. The message is any expression and is evaluated only when the guard fires, so a guard can say what it knows:

```ts
fail("not_found", `there is no post ${params.slug}`);
```

## Revalidation

An action that succeeds re-runs the loaders whose data it invalidated. The pages and layouts holding that data re-render in place. A layout takes its new props without losing its DOM, so a cart count follows a mutation without the search box beside it losing what was typed.

## The session and identity

The session is a typed record the host keeps, keyed by the cookie named in `[session]`. It survives a reload: the host rebuilds its tables and swaps them without signing anybody out.

The expiry does not slide. `ttl` is how long a new session lasts. It is one number written on the record. The cookie's `Max-Age` and the store both follow it, so a session read a thousand times still ends one `ttl` after it opened. Only the application moves it:

```ts
session.extend(7200);
```

An action, a route handler or middleware may call it. A loader may not; the build refuses it by name, since a loader runs on every navigation and extending there would be a store write per page view. Extend on sign-in, on a write the application was making anyway or once the time left drops under a threshold.

`identity` is separate and is what `[auth]` produced. A loader or an action sees it as `Identity | null` and guards on it without ever seeing a password. That separation lets a mounted site guard its own routes against the shell's sign-in.

```toml
[session]
key = "..."
ttl = "24h"

[auth]
provider = "file"
login = "/login"
```

Note that a mounted site's own `[session]` and `[auth]` are ignored: it uses the shell's. The boot report lists them under `ignored` so the drop is visible rather than silent.

The keys are a ring rather than one value. `previous_keys` holds the ones that still verify, so a rotation is a reload rather than a restart: the new `key` signs, the old ones verify until you take them out and nobody is signed out on the way. Every other part of `[session]` is refused on reload, because the store outlives it.

## Try it

Write an action whose guard fails, call it from the browser and watch the network tab: the request comes back 401 without the service behind it ever being contacted. Then move the guard below the service call and watch the socket open.
