Advanced · for app developers

Actions and the session

An action is a declared mutation

A loader reads. An action writes. It lives in actions.ts beside the page and is declared, not merely 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, and a call that does not typecheck does not compile.

Guards run before anything opens

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, and its kind maps onto a status:

KindStatus
unauthorized401
not_found404
invalid400
conflict409
timeout408
unavailable503
internal500

fail takes string literals. A message built by interpolation will not lower, which is deliberate: the diagnostic set is fixed at build time.

Revalidation

An action that succeeds re-runs the loaders whose data it invalidated, and 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, and nobody is signed out.

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 is what 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 lab

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.

Built with SnapFire FSR. Pure Rust runtime, zero Node.js on the server.