# Server sessions (https://session-kit.pages.dev/docs/server-rendering/server-sessions)

This page covers what every server-rendered setup shares: the cookie and its
secret, a request's session, refreshes, sign-out, and limits. Read it once
your app works, to deploy it. The setup guides show the code:
[TanStack Start](https://session-kit.pages.dev/docs/setups/tanstack-start), [Next.js](https://session-kit.pages.dev/docs/setups/next),
and [React Router with server rendering](https://session-kit.pages.dev/docs/setups/react-router-ssr).

## The cookie

`createServerAuth` from `@monarcode/session-kit/server` keeps both tokens and
the user in one cookie, `<name>.session`, sealed with AES-256-GCM through Web
Crypto. It's `httpOnly`, so page scripts can't read it, and `Secure` and
`SameSite=Lax` unless you pass other `cookie` options. It takes the options
`createAuth` does, except `storage` and `locking`, plus `secret`, `cookie`
and `clientCredentials`.

Keep the module that creates it on the server. Each setup guide shows how
its framework keeps such a module out of the browser bundle.

## The secret

Use 32 or more random characters from an environment variable. To make one,
run `openssl rand -base64 32`. `createServerAuth` checks the secret on first
use, not when the module loads, so build steps that import the module without
the secret still work.

Your module still reads `process.env.SESSION_SECRET` when it loads. On
Cloudflare Workers, `process.env` has the secret at that point when Node.js
compatibility is on, the default from compatibility date 2025-04-01. Without
Node.js compatibility, read it with `import { env } from "cloudflare:workers"`,
which also works at the top of a module.

To rotate the secret, pass a list. The first secret seals new cookies, and
any of them opens one, so sessions survive the change. For example, to read
the old secret from `OLD_SESSION_SECRET` only while it's set:

```ts title="src/auth/server.ts"
import { createServerAuth } from "@monarcode/session-kit/server";
import { z } from "zod";

const secrets = [process.env.SESSION_SECRET, process.env.OLD_SESSION_SECRET];

export const serverAuth = createServerAuth({
  name: "app",
  user: z.object({ id: z.number(), firstName: z.string() }),
  // A secret that isn't set is left out, so rotating only changes the env.
  secret: secrets.filter((secret) => secret !== undefined),
});
```

Each save seals the cookie with the first secret. Remove
`OLD_SESSION_SECRET` once `maxAge` has passed since the change, 30 days by
default. By then, every cookie it sealed has expired. A cookie that no listed
secret opens reads as signed out.

## A request's session

Your server code reads and changes the session through one request's
session (`RequestAuth`). It has every `serverAuth` method, without the
request's cookie jar to pass:

- **TanStack Start:** `startAuth(serverAuth)`, made once at module level.
  Each call finds the request Start is handling.
- **React Router:** `routerAuth(serverAuth)` makes a function you call with
  a loader's, action's or middleware's `context`.
- **Next.js:** `nextSession(serverAuth, { cookies, redirect })`, whose
  `writable()` gives it in Server Functions and Route Handlers, and
  `readOnly()` in Server Components.
- **Servers without an adapter:** `serverAuth.forRequest(jar)`, over the
  request's cookie jar.

It's shaped like a client, so code written for one works on the server with
it. `createAuthFetch(session, { baseUrl })` sends the cookie's token and saves
a renewed one after a 401, and Effect's `Auth.layer` takes it
([Effect on the server](https://session-kit.pages.dev/docs/effect#on-the-server)).

Some requests can't set cookies: a Next.js Server Component rendering, or a
React Router loader once its response has left the middleware. There,
nothing is saved, so `signIn`, `signOut`, `updateUser` and `refreshIfDue`
reject with `INVALID_OPTIONS`. `getSession` still resolves, and
`credentials.get` rejects with `REFRESH_FAILED` once the access token has
expired. TanStack Start can't tell once a page has started streaming, so a
cookie set then is lost. Refresh before rendering, with the middleware each
setup guide adds: `refreshSession` in TanStack Start, `sessionMiddleware` in
React Router, and `nextSession`'s `refresh` in Next.js's Proxy.

## Servers without an adapter

Every `serverAuth` method also takes the request's `CookieJar` first:
`serverAuth.getSession(jar)` does what a request's `getSession()` does, and
`serverAuth.forRequest(jar)` gives that request's session. For a server
without an adapter, build a [`CookieJar`](https://session-kit.pages.dev/docs/reference/server#cookiejar)
over its cookies. Answer the browser client's session route with
`sessionHandler(serverAuth)`, called with the request and its jar.

Use one jar per request. Calls with the same jar share what it read and
wrote and one refresh, so a jar kept at module level would show one
request's session to the next. A jar that can't set cookies says so with
`writable: false`.

## Token refresh

Parallel requests to one instance that carry the same cookie share one
refresh. For ten seconds afterwards, a request still carrying the old cookie
gets the new tokens rather than refreshing again. A refresh that fails at
your backend is shared too. For ten seconds, requests carrying that cookie
reject with its error rather than call your backend again.

Each `createServerAuth` call is its own instance, and a framework may make
several on one server, one per bundle. Separate processes, servers, and
serverless functions are separate instances too. Instances share nothing, so
with rotating refresh tokens, give your backend a short reuse window. Two
instances that refresh the same session then don't sign it out.

## Sign-out

A request's `signOut` clears the cookie in this browser, then calls your
`revoke`. The cookie holds the whole session and the server keeps no list of
sessions, so a copy of the cookie made before sign-out still reads as signed
in. For ten seconds, the instance that signed out treats such a copy as
signed out, as it treats an old cookie after a refresh. After that, the copy
works for as long as its tokens do.

`revoke` is what ends the session: have your backend revoke the refresh
token there, so a copy can't refresh, and keep access tokens short-lived,
since the backend accepts one until it expires unless it checks for
revocation.

## The access token in the browser

Server code calls your API with a request's `credentials`. To call it from
the browser too, with `createAuthFetch` or effetch's `sessionBearer`, opt in
with `clientCredentials: true`. The session route then serves the access
token, and the browser's client gets it there. Page scripts hold the access
token in memory, never the refresh token, so a script injected into the page
can use the API while the page is open, as it could with any token the
browser holds.

The session route answers your own site's pages only: see
[The session route](https://session-kit.pages.dev/docs/security#the-session-route).

## Limits

- **Size.** The cookie can take about 7.5 KB across two cookies, since
  servers and proxies refuse larger request headers (nginx: 8 KB for the
  `Cookie` line, shared with the site's other cookies), and go on refusing
  every page until the cookie is cleared. That holds about 5.5 KB of tokens
  and user before encryption. A larger session rejects at sign-in with
  `PERSISTENCE_FAILED`, so keep the user small. An `updateUser` or a refresh
  that outgrows the cookie rejects the same way, and signs the user out.
- **Plain http.** The cookie is `Secure` by default. Browsers accept that on
  `http://localhost` except Safari: pass `cookie: { secure: false }` for
  local development there, never in production.
- **Locking.** `locking` isn't available. The server keeps the tokens, so
  there's nothing on the device to lock.

## Next steps

- [Effect on the server](https://session-kit.pages.dev/docs/effect#on-the-server): run Effects with a
  request's session.
- [Security](https://session-kit.pages.dev/docs/security): what Session kit protects, and what stays your
  backend's job.