---
title: Sessions and tokens
description: What a session is made of, and how it stays alive.
---

import Callout from '../../../components/Callout.astro';


Signing in produces two tokens.

**An access token.** A short-lived signed JWT. This is what you send to your
own API, and what your API verifies. It carries who the person is, which
session it belongs to, and which organization the session is acting as.

**A refresh token.** An opaque string, longer-lived, used only to get a new
access token. It never goes to your API.

## Rotation, and why it is strict

Every refresh **rotates** the refresh token: the old one is spent and a new one
issued. If a spent token is ever presented again, the entire session is
revoked on every device, immediately.

That is deliberate. A replayed token means either a stolen copy is being used
or the real client raced itself, and the server cannot tell those apart. The
safe answer is the same in both cases: end the session. An attacker gets at
most one refresh before the real user's next refresh makes the theft visible.

<Callout type="warning">
There is no grace window. A client that refreshes twice at the same moment ends
its own session, on every device. `@zevauth/js` serialises refreshes across
tabs with a browser lock. If you write your own client, you have to do the
same.
</Callout>

## Where tokens live

The SDK keeps the access token **in memory** and persists only the refresh
token. On a fresh page load it exchanges the refresh token for a new access
token, which takes one request and starts the session clean.

## Revocation

Signing out revokes the session. So does changing a password, which ends every
**other** session and keeps the one making the change. Signing somebody out of
the browser they just used to secure their account punishes exactly the
behaviour you want.

A revoked session cannot be detected by verifying a token offline; that is what
stateless tokens trade away. Access tokens are short-lived so the window is
small, and `/v1/me` checks the session if you need certainty.

## Claims

```json
{
  "sub": "user_2xK…",
  "sid": "sess_9fQ…",
  "iss": "https://api.zevauth.net/v1/env_live_…",
  "aud": "env_live_…",
  "exp": 1767225600,
  "email": "ada@example.com",
  "email_verified": true,
  "public_metadata": {},
  "org_id": "org_7bC…",
  "org_role": "admin",
  "org_permissions": ["org:members:read"]
}
```

`public_metadata` is yours to set from your backend and is readable by anyone
holding the token, including the user. Private metadata never leaves your
server, and a user cannot write either.